diff --git a/packages/backend/src/apps/cryptography/actions/create-hmac/index.js b/packages/backend/src/apps/cryptography/actions/create-hmac/index.js new file mode 100644 index 00000000..e14f0d67 --- /dev/null +++ b/packages/backend/src/apps/cryptography/actions/create-hmac/index.js @@ -0,0 +1,64 @@ +import { createHmac } from 'node:crypto'; +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create HMAC', + key: 'createHmac', + description: 'Create a Hash-based Message Authentication Code (HMAC) using the specified algorithm, secret key, and message.', + arguments: [ + { + label: 'Algorithm', + key: 'algorithm', + type: 'dropdown', + required: true, + value: 'sha256', + description: 'Specifies the cryptographic hash function to use for HMAC generation.', + options: [ + { label: 'SHA-256', value: 'sha256' }, + ], + variables: true, + }, + { + label: 'Message', + key: 'message', + type: 'string', + required: true, + description: 'The input message to be hashed. This is the value that will be processed to generate the HMAC.', + variables: true, + }, + { + label: 'Secret Key', + key: 'secretKey', + type: 'string', + required: true, + description: 'The secret key used to create the HMAC.', + variables: true, + }, + { + label: 'Output Encoding', + key: 'outputEncoding', + type: 'dropdown', + required: true, + value: 'hex', + description: 'Specifies the encoding format for the HMAC digest output.', + options: [ + { label: 'base64', value: 'base64' }, + { label: 'base64url', value: 'base64url' }, + { label: 'hex', value: 'hex' }, + ], + variables: true, + }, + ], + + async run($) { + const hash = createHmac($.step.parameters.algorithm, $.step.parameters.secretKey) + .update($.step.parameters.message) + .digest($.step.parameters.outputEncoding); + + $.setActionItem({ + raw: { + hash + }, + }); + }, +}); diff --git a/packages/backend/src/apps/cryptography/actions/create-rsa-sha256-signature/index.js b/packages/backend/src/apps/cryptography/actions/create-rsa-sha256-signature/index.js new file mode 100644 index 00000000..446309e8 --- /dev/null +++ b/packages/backend/src/apps/cryptography/actions/create-rsa-sha256-signature/index.js @@ -0,0 +1,65 @@ +import crypto from 'node:crypto'; +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create Signature', + key: 'createSignature', + description: 'Create a digital signature using the specified algorithm, secret key, and message.', + arguments: [ + { + label: 'Algorithm', + key: 'algorithm', + type: 'dropdown', + required: true, + value: 'RSA-SHA256', + description: 'Specifies the cryptographic hash function to use for HMAC generation.', + options: [ + { label: 'RSA-SHA256', value: 'RSA-SHA256' }, + ], + variables: true, + }, + { + label: 'Message', + key: 'message', + type: 'string', + required: true, + description: 'The input message to be signed.', + variables: true, + }, + { + label: 'Private Key', + key: 'privateKey', + type: 'string', + required: true, + description: 'The RSA private key in PEM format used for signing.', + variables: true, + }, + { + label: 'Output Encoding', + key: 'outputEncoding', + type: 'dropdown', + required: true, + value: 'hex', + description: 'Specifies the encoding format for the digital signature output. This determines how the generated signature will be represented as a string.', + options: [ + { label: 'base64', value: 'base64' }, + { label: 'base64url', value: 'base64url' }, + { label: 'hex', value: 'hex' }, + ], + variables: true, + }, + ], + + async run($) { + const signer = crypto.createSign($.step.parameters.algorithm); + signer.update($.step.parameters.message); + signer.end(); + const signature = signer.sign($.step.parameters.privateKey, $.step.parameters.outputEncoding); + + $.setActionItem({ + raw: { + signature + }, + }); + }, +}); diff --git a/packages/backend/src/apps/cryptography/actions/index.js b/packages/backend/src/apps/cryptography/actions/index.js new file mode 100644 index 00000000..ab2da71e --- /dev/null +++ b/packages/backend/src/apps/cryptography/actions/index.js @@ -0,0 +1,4 @@ +import createHmac from './create-hmac/index.js'; +import createRsaSha256Signature from './create-rsa-sha256-signature/index.js'; + +export default [createHmac, createRsaSha256Signature]; diff --git a/packages/backend/src/apps/cryptography/assets/favicon.svg b/packages/backend/src/apps/cryptography/assets/favicon.svg new file mode 100644 index 00000000..da529327 --- /dev/null +++ b/packages/backend/src/apps/cryptography/assets/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/backend/src/apps/cryptography/index.js b/packages/backend/src/apps/cryptography/index.js new file mode 100644 index 00000000..23e57b2e --- /dev/null +++ b/packages/backend/src/apps/cryptography/index.js @@ -0,0 +1,14 @@ +import defineApp from '../../helpers/define-app.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Cryptography', + key: 'cryptography', + iconUrl: '{BASE_URL}/apps/cryptography/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/cryptography/connection', + supportsConnections: false, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '001F52', + actions, +}); diff --git a/packages/backend/src/apps/formatter/actions/date-time/index.js b/packages/backend/src/apps/formatter/actions/date-time/index.js index 830421d7..88fbecbe 100644 --- a/packages/backend/src/apps/formatter/actions/date-time/index.js +++ b/packages/backend/src/apps/formatter/actions/date-time/index.js @@ -1,8 +1,10 @@ import defineAction from '../../../../helpers/define-action.js'; import formatDateTime from './transformers/format-date-time.js'; +import getCurrentTimestamp from './transformers/get-current-timestamp.js'; const transformers = { formatDateTime, + getCurrentTimestamp, }; export default defineAction({ @@ -16,7 +18,16 @@ export default defineAction({ type: 'dropdown', required: true, variables: true, - options: [{ label: 'Format Date / Time', value: 'formatDateTime' }], + options: [ + { + label: 'Get current timestamp', + value: 'getCurrentTimestamp', + }, + { + label: 'Format Date / Time', + value: 'formatDateTime', + }, + ], additionalFields: { type: 'query', name: 'getDynamicFields', diff --git a/packages/backend/src/apps/formatter/actions/date-time/transformers/get-current-timestamp.js b/packages/backend/src/apps/formatter/actions/date-time/transformers/get-current-timestamp.js new file mode 100644 index 00000000..a0d7f0c2 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/date-time/transformers/get-current-timestamp.js @@ -0,0 +1,5 @@ +const getCurrentTimestamp = () => { + return Date.now(); +}; + +export default getCurrentTimestamp; diff --git a/packages/backend/src/apps/formatter/actions/text/index.js b/packages/backend/src/apps/formatter/actions/text/index.js index b2c7ee60..e33160ad 100644 --- a/packages/backend/src/apps/formatter/actions/text/index.js +++ b/packages/backend/src/apps/formatter/actions/text/index.js @@ -15,6 +15,7 @@ import encodeUri from './transformers/encode-uri.js'; import trimWhitespace from './transformers/trim-whitespace.js'; import useDefaultValue from './transformers/use-default-value.js'; import parseStringifiedJson from './transformers/parse-stringified-json.js'; +import createUuid from './transformers/create-uuid.js'; const transformers = { base64ToString, @@ -32,6 +33,7 @@ const transformers = { trimWhitespace, useDefaultValue, parseStringifiedJson, + createUuid, }; export default defineAction({ @@ -49,22 +51,23 @@ export default defineAction({ options: [ { label: 'Base64 to String', value: 'base64ToString' }, { label: 'Capitalize', value: 'capitalize' }, + { label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' }, + { label: 'Convert Markdown to HTML', value: 'markdownToHtml' }, + { label: 'Create UUID', value: 'createUuid' }, + { label: 'Encode URI', value: 'encodeUri' }, { label: 'Encode URI Component', value: 'encodeUriComponent', }, - { label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' }, - { label: 'Convert Markdown to HTML', value: 'markdownToHtml' }, { label: 'Extract Email Address', value: 'extractEmailAddress' }, { label: 'Extract Number', value: 'extractNumber' }, { label: 'Lowercase', value: 'lowercase' }, + { label: 'Parse stringified JSON', value: 'parseStringifiedJson' }, { label: 'Pluralize', value: 'pluralize' }, { label: 'Replace', value: 'replace' }, { label: 'String to Base64', value: 'stringToBase64' }, - { label: 'Encode URI', value: 'encodeUri' }, { label: 'Trim Whitespace', value: 'trimWhitespace' }, { label: 'Use Default Value', value: 'useDefaultValue' }, - { label: 'Parse stringified JSON', value: 'parseStringifiedJson' }, ], additionalFields: { type: 'query', diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/create-uuid.js b/packages/backend/src/apps/formatter/actions/text/transformers/create-uuid.js new file mode 100644 index 00000000..20d1ecc3 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/create-uuid.js @@ -0,0 +1,7 @@ +import { v4 as uuidv4 } from 'uuid'; + +const createUuidV4 = () => { + return uuidv4(); +}; + +export default createUuidV4; diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js index cacd3936..4d60ae53 100644 --- a/packages/backend/src/graphql/mutation-resolvers.js +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -29,11 +29,6 @@ import upsertSamlAuthProvider from './mutations/upsert-saml-auth-provider.ee.js' import upsertSamlAuthProvidersRoleMappings from './mutations/upsert-saml-auth-providers-role-mappings.ee.js'; import verifyConnection from './mutations/verify-connection.js'; -// Converted mutations -import deleteUser from './mutations/delete-user.ee.js'; -import login from './mutations/login.js'; -import resetPassword from './mutations/reset-password.ee.js'; - const mutationResolvers = { createAppAuthClient, createAppConfig, @@ -47,14 +42,11 @@ const mutationResolvers = { deleteFlow, deleteRole, deleteStep, - deleteUser, duplicateFlow, executeFlow, generateAuthUrl, - login, registerUser, resetConnection, - resetPassword, updateAppAuthClient, updateAppConfig, updateConfig, diff --git a/packages/backend/src/graphql/mutations/delete-user.ee.js b/packages/backend/src/graphql/mutations/delete-user.ee.js deleted file mode 100644 index 7b66dd05..00000000 --- a/packages/backend/src/graphql/mutations/delete-user.ee.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Duration } from 'luxon'; -import User from '../../models/user.js'; -import deleteUserQueue from '../../queues/delete-user.ee.js'; - -const deleteUser = async (_parent, params, context) => { - context.currentUser.can('delete', 'User'); - - const id = params.input.id; - - await User.query().deleteById(id); - - const jobName = `Delete user - ${id}`; - const jobPayload = { id }; - const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis(); - const jobOptions = { - delay: millisecondsFor30Days, - }; - - await deleteUserQueue.add(jobName, jobPayload, jobOptions); - - return true; -}; - -export default deleteUser; diff --git a/packages/backend/src/graphql/mutations/login.js b/packages/backend/src/graphql/mutations/login.js deleted file mode 100644 index b0504570..00000000 --- a/packages/backend/src/graphql/mutations/login.js +++ /dev/null @@ -1,17 +0,0 @@ -import User from '../../models/user.js'; -import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id.js'; - -const login = async (_parent, params) => { - const user = await User.query().findOne({ - email: params.input.email.toLowerCase(), - }); - - if (user && (await user.login(params.input.password))) { - const token = await createAuthTokenByUserId(user.id); - return { token, user }; - } - - throw new Error('User could not be found.'); -}; - -export default login; diff --git a/packages/backend/src/graphql/mutations/reset-password.ee.js b/packages/backend/src/graphql/mutations/reset-password.ee.js deleted file mode 100644 index 309b006a..00000000 --- a/packages/backend/src/graphql/mutations/reset-password.ee.js +++ /dev/null @@ -1,23 +0,0 @@ -import User from '../../models/user.js'; - -const resetPassword = async (_parent, params) => { - const { token, password } = params.input; - - if (!token) { - throw new Error('Reset password token is required!'); - } - - const user = await User.query().findOne({ reset_password_token: token }); - - if (!user || !user.isResetPasswordTokenValid()) { - throw new Error( - 'Reset password link is not valid or expired. Try generating a new link.' - ); - } - - await user.resetPassword(password); - - return true; -}; - -export default resetPassword; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index e64228fd..a398cb41 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -14,14 +14,11 @@ type Mutation { deleteFlow(input: DeleteFlowInput): Boolean deleteRole(input: DeleteRoleInput): Boolean deleteStep(input: DeleteStepInput): Step - deleteUser(input: DeleteUserInput): Boolean duplicateFlow(input: DuplicateFlowInput): Flow executeFlow(input: ExecuteFlowInput): executeFlowType generateAuthUrl(input: GenerateAuthUrlInput): AuthLink - login(input: LoginInput): Auth registerUser(input: RegisterUserInput): User resetConnection(input: ResetConnectionInput): Connection - resetPassword(input: ResetPasswordInput): Boolean updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient updateAppConfig(input: UpdateAppConfigInput): AppConfig updateConfig(input: JSONObject): JSONObject @@ -153,11 +150,6 @@ enum ArgumentEnumType { string } -type Auth { - user: User - token: String -} - type AuthenticationStep { type: String name: String @@ -388,10 +380,6 @@ input UpdateUserInput { role: UserRoleInput } -input DeleteUserInput { - id: String! -} - input RegisterUserInput { fullName: String! email: String! @@ -404,16 +392,6 @@ input UpdateCurrentUserInput { fullName: String } -input ResetPasswordInput { - token: String! - password: String! -} - -input LoginInput { - email: String! - password: String! -} - input PermissionInput { action: String! subject: String! diff --git a/packages/backend/src/helpers/authentication.js b/packages/backend/src/helpers/authentication.js index f57c41fa..9f01d21d 100644 --- a/packages/backend/src/helpers/authentication.js +++ b/packages/backend/src/helpers/authentication.js @@ -53,9 +53,7 @@ const isAuthenticatedRule = rule()(isAuthenticated); export const authenticationRules = { Mutation: { '*': isAuthenticatedRule, - login: allow, registerUser: allow, - resetPassword: allow, }, }; diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js index 219a1d51..216b6514 100644 --- a/packages/docs/pages/.vitepress/config.js +++ b/packages/docs/pages/.vitepress/config.js @@ -59,6 +59,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/carbone/connection' }, ], }, + { + text: 'Cryptography', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/cryptography/actions' }, + { text: 'Connection', link: '/apps/cryptography/connection' }, + ], + }, { text: 'Datastore', collapsible: true, diff --git a/packages/docs/pages/apps/cryptography/actions.md b/packages/docs/pages/apps/cryptography/actions.md new file mode 100644 index 00000000..a7aa2a77 --- /dev/null +++ b/packages/docs/pages/apps/cryptography/actions.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/cryptography.svg +items: + - name: Create HMAC + desc: Create a Hash-based Message Authentication Code (HMAC) using the specified algorithm, secret key, and message. + - name: Create Signature + desc: Create a digital signature using the specified algorithm, secret key, and message. +--- + + + + diff --git a/packages/docs/pages/apps/cryptography/connection.md b/packages/docs/pages/apps/cryptography/connection.md new file mode 100644 index 00000000..5cf28566 --- /dev/null +++ b/packages/docs/pages/apps/cryptography/connection.md @@ -0,0 +1,3 @@ +# Cryptography + +Cryptography is a built-in app shipped with Automatisch, allowing you to perform cryptographic operations without needing to connect to any external services. diff --git a/packages/docs/pages/public/favicons/cryptography.svg b/packages/docs/pages/public/favicons/cryptography.svg new file mode 100644 index 00000000..da529327 --- /dev/null +++ b/packages/docs/pages/public/favicons/cryptography.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/e2e-tests/.eslintignore b/packages/e2e-tests/.eslintignore new file mode 100644 index 00000000..e2699178 --- /dev/null +++ b/packages/e2e-tests/.eslintignore @@ -0,0 +1,6 @@ +node_modules +build + +.eslintrc.js + +playwright-report/* \ No newline at end of file diff --git a/packages/e2e-tests/.eslintrc.json b/packages/e2e-tests/.eslintrc.json new file mode 100644 index 00000000..943e6da1 --- /dev/null +++ b/packages/e2e-tests/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "root": true, + "env": { + "node": true, + "es6": true + }, + "extends": [ + "eslint:recommended", + "prettier" + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "semi": [ + 2, + "always" + ], + "indent": [ + "error", + 2 + ] + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/admin/delete-user-modal.js b/packages/e2e-tests/fixtures/admin/delete-user-modal.js index 6082d0e3..8a4f33ed 100644 --- a/packages/e2e-tests/fixtures/admin/delete-user-modal.js +++ b/packages/e2e-tests/fixtures/admin/delete-user-modal.js @@ -14,6 +14,6 @@ export class DeleteUserModal { async close () { await this.page.click('body', { position: { x: 10, y: 10 } - }) + }); } } \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/admin/edit-role-page.js b/packages/e2e-tests/fixtures/admin/edit-role-page.js index 9dd21dfe..9c9fec6b 100644 --- a/packages/e2e-tests/fixtures/admin/edit-role-page.js +++ b/packages/e2e-tests/fixtures/admin/edit-role-page.js @@ -1,4 +1,4 @@ -const { AdminCreateRolePage } = require('./create-role-page') +const { AdminCreateRolePage } = require('./create-role-page'); export class AdminEditRolePage extends AdminCreateRolePage { constructor (page) { diff --git a/packages/e2e-tests/fixtures/admin/edit-user-page.js b/packages/e2e-tests/fixtures/admin/edit-user-page.js index 9308294b..4bc3a1b6 100644 --- a/packages/e2e-tests/fixtures/admin/edit-user-page.js +++ b/packages/e2e-tests/fixtures/admin/edit-user-page.js @@ -23,6 +23,7 @@ export class AdminEditUserPage extends AuthenticatedPage { */ async waitForLoad(fullName) { return await this.page.waitForFunction((fullName) => { + // eslint-disable-next-line no-undef const el = document.querySelector("[data-test='full-name-input']"); return el && el.value === fullName; }, fullName); diff --git a/packages/e2e-tests/fixtures/admin/index.js b/packages/e2e-tests/fixtures/admin/index.js index 8c25fd7c..fe746243 100644 --- a/packages/e2e-tests/fixtures/admin/index.js +++ b/packages/e2e-tests/fixtures/admin/index.js @@ -25,5 +25,5 @@ export const adminFixtures = { adminCreateRolePage: async ({ page}, use) => { await use(new AdminCreateRolePage(page)); }, -} +}; diff --git a/packages/e2e-tests/fixtures/admin/users-page.js b/packages/e2e-tests/fixtures/admin/users-page.js index 4c96fd75..af6dbac3 100644 --- a/packages/e2e-tests/fixtures/admin/users-page.js +++ b/packages/e2e-tests/fixtures/admin/users-page.js @@ -87,6 +87,7 @@ export class AdminUsersPage extends AuthenticatedPage { await this.firstPageButton.click(); } + // eslint-disable-next-line no-constant-condition while (true) { if (await this.usersLoader.isVisible()) { await this.usersLoader.waitFor({ @@ -108,6 +109,7 @@ export class AdminUsersPage extends AuthenticatedPage { async getTotalRows() { return await this.page.evaluate(() => { + // eslint-disable-next-line no-undef const node = document.querySelector('[data-total-count]'); if (node) { const count = Number(node.dataset.totalCount); @@ -121,6 +123,7 @@ export class AdminUsersPage extends AuthenticatedPage { async getRowsPerPage() { return await this.page.evaluate(() => { + // eslint-disable-next-line no-undef const node = document.querySelector('[data-rows-per-page]'); if (node) { const count = Number(node.dataset.rowsPerPage); diff --git a/packages/e2e-tests/fixtures/applications-modal.js b/packages/e2e-tests/fixtures/applications-modal.js index 8833087f..f9bab969 100644 --- a/packages/e2e-tests/fixtures/applications-modal.js +++ b/packages/e2e-tests/fixtures/applications-modal.js @@ -25,7 +25,7 @@ export class ApplicationsModal extends BasePage { if (this.applications[link] === undefined) { throw { message: `Unknown link "${link}" passed to ApplicationsModal.selectLink` - } + }; } await this.searchInput.fill(link); await this.appListItem.first().click(); diff --git a/packages/e2e-tests/fixtures/apps/github/github-page.js b/packages/e2e-tests/fixtures/apps/github/github-page.js index f1a05d5f..d826c0d4 100644 --- a/packages/e2e-tests/fixtures/apps/github/github-page.js +++ b/packages/e2e-tests/fixtures/apps/github/github-page.js @@ -1,10 +1,11 @@ const { BasePage } = require('../../base-page'); const { AddGithubConnectionModal } = require('./add-github-connection-modal'); +const { expect } = require('@playwright/test'); export class GithubPage extends BasePage { constructor (page) { - super(page) + super(page); this.addConnectionButton = page.getByTestId('add-connection-button'); this.connectionsTab = page.getByTestId('connections-tab'); this.flowsTab = page.getByTestId('flows-tab'); @@ -38,7 +39,7 @@ export class GithubPage extends BasePage { await this.flowsTab.click(); await expect(this.flowsTab).toBeVisible(); } - return await this.flowRows.count() > 0 + return await this.flowRows.count() > 0; } async hasConnections () { diff --git a/packages/e2e-tests/fixtures/apps/github/github-popup.js b/packages/e2e-tests/fixtures/apps/github/github-popup.js index 0bdb8579..f1c8e2fe 100644 --- a/packages/e2e-tests/fixtures/apps/github/github-popup.js +++ b/packages/e2e-tests/fixtures/apps/github/github-popup.js @@ -1,4 +1,5 @@ const { BasePage } = require('../../base-page'); +const { expect } = require('@playwright/test'); export class GithubPopup extends BasePage { @@ -11,7 +12,7 @@ export class GithubPopup extends BasePage { } getPathname () { - const url = this.page.url() + const url = this.page.url(); try { return new URL(url).pathname; } catch (e) { @@ -34,17 +35,17 @@ export class GithubPopup extends BasePage { loginInput.click(); await loginInput.fill(process.env.GITHUB_USERNAME); const passwordInput = this.page.getByLabel('Password'); - passwordInput.click() + passwordInput.click(); await passwordInput.fill(process.env.GITHUB_PASSWORD); await this.page.getByRole('button', { name: 'Sign in' }).click(); // await this.page.waitForTimeout(2000); if (this.page.isClosed()) { - return + return; } // await this.page.waitForLoadState('networkidle', 30000); this.page.waitForEvent('load'); if (this.page.isClosed()) { - return + return; } await this.page.waitForURL(function (url) { const u = new URL(url); @@ -55,7 +56,7 @@ export class GithubPopup extends BasePage { } async handleAuthorize () { - if (this.page.isClosed()) { return } + if (this.page.isClosed()) { return; } const authorizeButton = this.page.getByRole( 'button', { name: 'Authorize' } @@ -69,7 +70,7 @@ export class GithubPopup extends BasePage { ) && ( u.searchParams.get('client_id') === null ); - }) + }); const passwordInput = this.page.getByLabel('Password'); if (await passwordInput.isVisible()) { await passwordInput.fill(process.env.GITHUB_PASSWORD); @@ -87,6 +88,6 @@ export class GithubPopup extends BasePage { }; } } - await this.page.waitForEvent('close') + await this.page.waitForEvent('close'); } } \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/authenticated-page.js b/packages/e2e-tests/fixtures/authenticated-page.js index d0d653be..ceda6f6f 100644 --- a/packages/e2e-tests/fixtures/authenticated-page.js +++ b/packages/e2e-tests/fixtures/authenticated-page.js @@ -1,7 +1,4 @@ -const path = require('node:path'); -const { expect } = require('@playwright/test'); const { BasePage } = require('./base-page'); -const { LoginPage } = require('./login-page'); export class AuthenticatedPage extends BasePage { /** diff --git a/packages/e2e-tests/fixtures/connections-page.js b/packages/e2e-tests/fixtures/connections-page.js index ad2c7fc8..a0ef8c42 100644 --- a/packages/e2e-tests/fixtures/connections-page.js +++ b/packages/e2e-tests/fixtures/connections-page.js @@ -1,4 +1,3 @@ -const path = require('node:path'); const { AuthenticatedPage } = require('./authenticated-page'); export class ConnectionsPage extends AuthenticatedPage { diff --git a/packages/e2e-tests/fixtures/executions-page.js b/packages/e2e-tests/fixtures/executions-page.js index 4a00782a..41b673e3 100644 --- a/packages/e2e-tests/fixtures/executions-page.js +++ b/packages/e2e-tests/fixtures/executions-page.js @@ -1,4 +1,3 @@ -const path = require('node:path'); const { AuthenticatedPage } = require('./authenticated-page'); export class ExecutionsPage extends AuthenticatedPage { diff --git a/packages/e2e-tests/fixtures/flow-editor-page.js b/packages/e2e-tests/fixtures/flow-editor-page.js index d0383525..b7de2d98 100644 --- a/packages/e2e-tests/fixtures/flow-editor-page.js +++ b/packages/e2e-tests/fixtures/flow-editor-page.js @@ -1,4 +1,3 @@ -const path = require('node:path'); const { AuthenticatedPage } = require('./authenticated-page'); export class FlowEditorPage extends AuthenticatedPage { diff --git a/packages/e2e-tests/fixtures/index.js b/packages/e2e-tests/fixtures/index.js index d9fab044..f9376786 100644 --- a/packages/e2e-tests/fixtures/index.js +++ b/packages/e2e-tests/fixtures/index.js @@ -8,6 +8,7 @@ const { LoginPage } = require('./login-page'); const { AcceptInvitation } = require('./accept-invitation-page'); const { adminFixtures } = require('./admin'); const { AdminSetupPage } = require('./admin-setup-page'); +const { AdminCreateUserPage } = require('./admin/create-user-page'); exports.test = test.extend({ page: async ({ page }, use) => { @@ -58,6 +59,11 @@ exports.publicTest = test.extend({ const adminSetupPage = new AdminSetupPage(page); await use(adminSetupPage); }, + + adminCreateUserPage: async ({page}, use) => { + const adminCreateUserPage = new AdminCreateUserPage(page); + await use(adminCreateUserPage); + } }); expect.extend({ diff --git a/packages/e2e-tests/fixtures/postgres-client-config.js b/packages/e2e-tests/fixtures/postgres-client-config.js index 076b6b8f..8ebc831d 100644 --- a/packages/e2e-tests/fixtures/postgres-client-config.js +++ b/packages/e2e-tests/fixtures/postgres-client-config.js @@ -1,11 +1,11 @@ const { Client } = require('pg'); const client = new Client({ - host: process.env.POSTGRES_HOST, - user: process.env.POSTGRES_USERNAME, - port: process.env.POSTGRES_PORT, - password: process.env.POSTGRES_PASSWORD, - database: process.env.POSTGRES_DATABASE + host: process.env.POSTGRES_HOST, + user: process.env.POSTGRES_USERNAME, + port: process.env.POSTGRES_PORT, + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DATABASE }); exports.client = client; diff --git a/packages/e2e-tests/fixtures/user-interface-page.js b/packages/e2e-tests/fixtures/user-interface-page.js index 119b92b1..6342af3a 100644 --- a/packages/e2e-tests/fixtures/user-interface-page.js +++ b/packages/e2e-tests/fixtures/user-interface-page.js @@ -1,4 +1,3 @@ -const path = require('node:path'); const { AuthenticatedPage } = require('./authenticated-page'); export class UserInterfacePage extends AuthenticatedPage { diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 1e731307..4bca4344 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -7,7 +7,8 @@ "scripts": { "start-mock-license-server": "node ./license-server-with-mock.js", "test": "playwright test", - "test:fast": "yarn test -j 90% --quiet --reporter null --ignore-snapshots -x" + "test:fast": "yarn test -j 90% --quiet --reporter null --ignore-snapshots -x", + "lint": "eslint ." }, "contributors": [ { diff --git a/packages/e2e-tests/tests/admin/manage-roles.spec.js b/packages/e2e-tests/tests/admin/manage-roles.spec.js index 7a4317d6..fa9eace1 100644 --- a/packages/e2e-tests/tests/admin/manage-roles.spec.js +++ b/packages/e2e-tests/tests/admin/manage-roles.spec.js @@ -17,7 +17,6 @@ test.describe('Role management page', () => { adminCreateRolePage, adminEditRolePage, adminRolesPage, - page, }) => { await test.step('Create a new role', async () => { await adminRolesPage.navigateTo(); @@ -126,12 +125,14 @@ test.describe('Role management page', () => { await adminCreateRolePage.isMounted(); const initScrollTop = await page.evaluate(() => { + // eslint-disable-next-line no-undef return document.documentElement.scrollTop; }); await page.mouse.move(400, 100); await page.mouse.click(400, 100); await page.mouse.wheel(200, 0); const updatedScrollTop = await page.evaluate(() => { + // eslint-disable-next-line no-undef return document.documentElement.scrollTop; }); await expect(initScrollTop).not.toBe(updatedScrollTop); @@ -144,11 +145,13 @@ test.describe('Role management page', () => { await adminEditRolePage.isMounted(); const initScrollTop = await page.evaluate(() => { + // eslint-disable-next-line no-undef return document.documentElement.scrollTop; }); await page.mouse.move(400, 100); await page.mouse.wheel(200, 0); const updatedScrollTop = await page.evaluate(() => { + // eslint-disable-next-line no-undef return document.documentElement.scrollTop; }); await expect(initScrollTop).not.toBe(updatedScrollTop); @@ -165,7 +168,6 @@ test.describe('Role management page', () => { adminUsersPage, adminCreateUserPage, adminEditUserPage, - page, }) => { await adminRolesPage.navigateTo(); await test.step('Create a new role', async () => { @@ -270,7 +272,6 @@ test.describe('Role management page', () => { adminRolesPage, adminUsersPage, adminCreateUserPage, - page, }) => { await adminRolesPage.navigateTo(); await test.step('Create a new role', async () => { @@ -429,6 +430,7 @@ test('Accessibility of role management page', async ({ await page.goto(url); await page.waitForTimeout(750); const isUnmounted = await page.evaluate(() => { + // eslint-disable-next-line no-undef const root = document.querySelector('#root'); if (root) { diff --git a/packages/e2e-tests/tests/admin/manage-users.spec.js b/packages/e2e-tests/tests/admin/manage-users.spec.js index fc3c324b..80ebfc07 100644 --- a/packages/e2e-tests/tests/admin/manage-users.spec.js +++ b/packages/e2e-tests/tests/admin/manage-users.spec.js @@ -98,7 +98,7 @@ test.describe('User management page', () => { await expect(userRow).not.toBeVisible(false); } ); - }); + }); test( 'Creating a user which has been deleted', diff --git a/packages/e2e-tests/tests/admin/role-conditions.spec.js b/packages/e2e-tests/tests/admin/role-conditions.spec.js index 6f69ad58..1b738406 100644 --- a/packages/e2e-tests/tests/admin/role-conditions.spec.js +++ b/packages/e2e-tests/tests/admin/role-conditions.spec.js @@ -51,7 +51,7 @@ test( const subjects = ['Connection', 'Execution', 'Flow']; for (let subject of subjects) { - const row = adminCreateRolePage.getSubjectRow(subject) + const row = adminCreateRolePage.getSubjectRow(subject); const modal = adminCreateRolePage.getRoleConditionsModal(subject); await adminCreateRolePage.clickPermissionSettings(row); await expect(modal.modal).toBeVisible(); diff --git a/packages/e2e-tests/tests/executions/display-execution.spec.js b/packages/e2e-tests/tests/executions/display-execution.spec.js index 8e1b4995..8de79dd7 100644 --- a/packages/e2e-tests/tests/executions/display-execution.spec.js +++ b/packages/e2e-tests/tests/executions/display-execution.spec.js @@ -2,7 +2,7 @@ const { test, expect } = require('../../fixtures/index'); // no execution data exists in an empty account test.describe.skip('Executions page', () => { - test.beforeEach(async ({ page, executionsPage }) => { + test.beforeEach(async ({ page }) => { await page.getByTestId('executions-page-drawer-link').click(); await page.getByTestId('execution-row').first().click(); diff --git a/packages/e2e-tests/tests/flow-editor/create-flow.spec.js b/packages/e2e-tests/tests/flow-editor/create-flow.spec.js index 363cad57..43d82861 100644 --- a/packages/e2e-tests/tests/flow-editor/create-flow.spec.js +++ b/packages/e2e-tests/tests/flow-editor/create-flow.spec.js @@ -6,7 +6,7 @@ test('Ensure creating a new flow works', async ({ page }) => { await expect(page).toHaveURL( /\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/ ); -}) +}); test( 'Create a new flow with a Scheduler step then an Ntfy step', diff --git a/packages/e2e-tests/tests/user-invitation/invitation.spec.js b/packages/e2e-tests/tests/user-invitation/invitation.spec.js index 63a2f13e..33bc4378 100644 --- a/packages/e2e-tests/tests/user-invitation/invitation.spec.js +++ b/packages/e2e-tests/tests/user-invitation/invitation.spec.js @@ -1,17 +1,8 @@ -const { AdminCreateUserPage } = require('../../fixtures/admin/create-user-page'); const { publicTest, expect } = require('../../fixtures/index'); const { client } = require('../../fixtures/postgres-client-config'); const { DateTime } = require('luxon'); publicTest.describe('Accept invitation page', () => { - publicTest.beforeAll(async () => { - await client.connect(); - }); - - publicTest.afterAll(async () => { - await client.end(); - }); - publicTest('should not be able to set the password if token is empty', async ({ acceptInvitationPage }) => { await acceptInvitationPage.open(''); await acceptInvitationPage.excpectSubmitButtonToBeDisabled(); @@ -19,44 +10,83 @@ publicTest.describe('Accept invitation page', () => { await acceptInvitationPage.excpectSubmitButtonToBeDisabled(); }); - publicTest('should not be able to set the password if token is expired', async ({ acceptInvitationPage, page }) => { - const expiredTokenDate = DateTime.now().minus({days: 3}).toISO(); - const expiredToken = (Math.random() + 1).toString(36).substring(2); - - const adminCreateUserPage = new AdminCreateUserPage(page); - adminCreateUserPage.seed(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)); - const user = adminCreateUserPage.generateUser(); - - const queryRole = { - text: 'SELECT * FROM roles WHERE name = $1', - values: ['Admin'] - }; - - try { - const queryRoleIdResult = await client.query(queryRole); - expect(queryRoleIdResult.rowCount).toEqual(1); - - const insertUser = { - text: 'INSERT INTO users (email, full_name, role_id, status, invitation_token, invitation_token_sent_at) VALUES ($1, $2, $3, $4, $5, $6)', - values: [user.email, user.fullName, queryRoleIdResult.rows[0].id, 'invited', expiredToken, expiredTokenDate], - }; - - const insertUserResult = await client.query(insertUser); - expect(insertUserResult.rowCount).toBe(1); - expect(insertUserResult.command).toBe('INSERT'); - } catch (err) { - console.error(err.message); - throw err; - } - - await acceptInvitationPage.open(expiredToken); - await acceptInvitationPage.acceptInvitation('something'); - await acceptInvitationPage.expectAlertToBeVisible(); - }); - publicTest('should not be able to set the password if token is not in db', async ({ acceptInvitationPage }) => { await acceptInvitationPage.open('abc'); await acceptInvitationPage.acceptInvitation('something'); await acceptInvitationPage.expectAlertToBeVisible(); }); + + publicTest.describe('Accept invitation page - users', () => { + const expiredTokenDate = DateTime.now().minus({days: 3}).toISO(); + const token = (Math.random() + 1).toString(36).substring(2); + + publicTest.beforeAll(async () => { + await client.connect(); + }); + + publicTest.afterAll(async () => { + await client.end(); + }); + + publicTest('should not be able to set the password if token is expired', async ({ acceptInvitationPage, adminCreateUserPage }) => { + adminCreateUserPage.seed(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)); + const user = adminCreateUserPage.generateUser(); + + const queryRole = { + text: 'SELECT * FROM roles WHERE name = $1', + values: ['Admin'] + }; + + try { + const queryRoleIdResult = await client.query(queryRole); + expect(queryRoleIdResult.rowCount).toEqual(1); + + const insertUser = { + text: 'INSERT INTO users (email, full_name, role_id, status, invitation_token, invitation_token_sent_at) VALUES ($1, $2, $3, $4, $5, $6)', + values: [user.email, user.fullName, queryRoleIdResult.rows[0].id, 'invited', token, expiredTokenDate], + }; + + const insertUserResult = await client.query(insertUser); + expect(insertUserResult.rowCount).toBe(1); + expect(insertUserResult.command).toBe('INSERT'); + } catch (err) { + console.error(err.message); + throw err; + } + await acceptInvitationPage.open(token); + await acceptInvitationPage.acceptInvitation('something'); + await acceptInvitationPage.expectAlertToBeVisible(); + }); + + publicTest('should not be able to accept invitation if user was soft deleted', async ({ acceptInvitationPage, adminCreateUserPage }) => { + const dateNow = DateTime.now().toISO(); + const user = adminCreateUserPage.generateUser(); + + const queryRole = { + text: 'SELECT * FROM roles WHERE name = $1', + values: ['Admin'] + }; + + try { + const queryRoleIdResult = await client.query(queryRole); + expect(queryRoleIdResult.rowCount).toEqual(1); + + const insertUser = { + text: 'INSERT INTO users (email, full_name, deleted_at, role_id, status, invitation_token, invitation_token_sent_at) VALUES ($1, $2, $3, $4, $5, $6, $7)', + values: [user.email, user.fullName, dateNow, queryRoleIdResult.rows[0].id, 'invited', token, dateNow], + }; + + const insertUserResult = await client.query(insertUser); + expect(insertUserResult.rowCount).toBe(1); + expect(insertUserResult.command).toBe('INSERT'); + } catch (err) { + console.error(err.message); + throw err; + } + + await acceptInvitationPage.open(token); + await acceptInvitationPage.acceptInvitation('something'); + await acceptInvitationPage.expectAlertToBeVisible(); + }); + }); }); diff --git a/packages/web/src/components/AppConnectionRow/index.jsx b/packages/web/src/components/AppConnectionRow/index.jsx index 5e6dc0ac..3b76256c 100644 --- a/packages/web/src/components/AppConnectionRow/index.jsx +++ b/packages/web/src/components/AppConnectionRow/index.jsx @@ -10,7 +10,7 @@ import CardActionArea from '@mui/material/CardActionArea'; import CircularProgress from '@mui/material/CircularProgress'; import Stack from '@mui/material/Stack'; import { DateTime } from 'luxon'; - +import { useQueryClient } from '@tanstack/react-query'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import ConnectionContextMenu from 'components/AppConnectionContextMenu'; import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection'; @@ -35,6 +35,7 @@ function AppConnectionRow(props) { const [verificationVisible, setVerificationVisible] = React.useState(false); const contextButtonRef = React.useRef(null); const [anchorEl, setAnchorEl] = React.useState(null); + const queryClient = useQueryClient(); const [deleteConnection] = useMutation(DELETE_CONNECTION); @@ -75,6 +76,9 @@ function AppConnectionRow(props) { }, }); + await queryClient.invalidateQueries({ + queryKey: ['apps', key, 'connections'], + }); enqueueSnackbar(formatMessage('connection.deletedMessage'), { variant: 'success', SnackbarProps: { @@ -86,7 +90,7 @@ function AppConnectionRow(props) { testConnection({ variables: { id } }); } }, - [deleteConnection, id, testConnection, formatMessage, enqueueSnackbar], + [deleteConnection, id, queryClient, key, enqueueSnackbar, formatMessage, testConnection], ); const relativeCreatedAt = DateTime.fromMillis( diff --git a/packages/web/src/components/DeleteUserButton/index.ee.jsx b/packages/web/src/components/DeleteUserButton/index.ee.jsx index 7a28c7e2..de4b918f 100644 --- a/packages/web/src/components/DeleteUserButton/index.ee.jsx +++ b/packages/web/src/components/DeleteUserButton/index.ee.jsx @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import { useMutation } from '@apollo/client'; import DeleteIcon from '@mui/icons-material/Delete'; import IconButton from '@mui/material/IconButton'; import { useQueryClient } from '@tanstack/react-query'; @@ -7,16 +6,14 @@ import { useQueryClient } from '@tanstack/react-query'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import * as React from 'react'; import ConfirmationDialog from 'components/ConfirmationDialog'; -import { DELETE_USER } from 'graphql/mutations/delete-user.ee'; import useFormatMessage from 'hooks/useFormatMessage'; +import useAdminUserDelete from 'hooks/useAdminUserDelete'; function DeleteUserButton(props) { const { userId } = props; const [showConfirmation, setShowConfirmation] = React.useState(false); - const [deleteUser] = useMutation(DELETE_USER, { - variables: { input: { id: userId } }, - refetchQueries: ['GetUsers'], - }); + const { mutateAsync: deleteUser } = useAdminUserDelete(userId); + const formatMessage = useFormatMessage(); const enqueueSnackbar = useEnqueueSnackbar(); const queryClient = useQueryClient(); @@ -33,7 +30,12 @@ function DeleteUserButton(props) { }, }); } catch (error) { - throw new Error('Failed while deleting!'); + enqueueSnackbar( + error?.message || formatMessage('deleteUserButton.deleteError'), + { + variant: 'error', + }, + ); } }, [deleteUser]); diff --git a/packages/web/src/components/LoginForm/index.jsx b/packages/web/src/components/LoginForm/index.jsx index 6cf95fbc..3611417e 100644 --- a/packages/web/src/components/LoginForm/index.jsx +++ b/packages/web/src/components/LoginForm/index.jsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { useNavigate, Link as RouterLink } from 'react-router-dom'; -import { useMutation } from '@apollo/client'; import Paper from '@mui/material/Paper'; import Link from '@mui/material/Link'; import Typography from '@mui/material/Typography'; @@ -8,17 +7,20 @@ import LoadingButton from '@mui/lab/LoadingButton'; import useAuthentication from 'hooks/useAuthentication'; import useCloud from 'hooks/useCloud'; import * as URLS from 'config/urls'; -import { LOGIN } from 'graphql/mutations/login'; import Form from 'components/Form'; import TextField from 'components/TextField'; import useFormatMessage from 'hooks/useFormatMessage'; +import useCreateAccessToken from 'hooks/useCreateAccessToken'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; function LoginForm() { const isCloud = useCloud(); const navigate = useNavigate(); const formatMessage = useFormatMessage(); + const enqueueSnackbar = useEnqueueSnackbar(); const authentication = useAuthentication(); - const [login, { loading }] = useMutation(LOGIN); + const { mutateAsync: createAccessToken, isPending: loading } = + useCreateAccessToken(); React.useEffect(() => { if (authentication.isAuthenticated) { @@ -27,13 +29,19 @@ function LoginForm() { }, [authentication.isAuthenticated]); const handleSubmit = async (values) => { - const { data } = await login({ - variables: { - input: values, - }, - }); - const { token } = data.login; - authentication.updateToken(token); + try { + const { email, password } = values; + const { data } = await createAccessToken({ + email, + password, + }); + const { token } = data; + authentication.updateToken(token); + } catch (error) { + enqueueSnackbar(error?.message || formatMessage('loginForm.error'), { + variant: 'error', + }); + } }; return ( diff --git a/packages/web/src/components/ResetPasswordForm/index.ee.jsx b/packages/web/src/components/ResetPasswordForm/index.ee.jsx index 996311ab..4d4cdaaf 100644 --- a/packages/web/src/components/ResetPasswordForm/index.ee.jsx +++ b/packages/web/src/components/ResetPasswordForm/index.ee.jsx @@ -1,4 +1,3 @@ -import { useMutation } from '@apollo/client'; import { yupResolver } from '@hookform/resolvers/yup'; import LoadingButton from '@mui/lab/LoadingButton'; import Paper from '@mui/material/Paper'; @@ -7,11 +6,12 @@ import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import * as React from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import * as yup from 'yup'; + import Form from 'components/Form'; import TextField from 'components/TextField'; import * as URLS from 'config/urls'; -import { RESET_PASSWORD } from 'graphql/mutations/reset-password.ee'; import useFormatMessage from 'hooks/useFormatMessage'; +import useResetPassword from 'hooks/useResetPassword'; const validationSchema = yup.object().shape({ password: yup.string().required('resetPasswordForm.mandatoryInput'), @@ -26,25 +26,35 @@ export default function ResetPasswordForm() { const formatMessage = useFormatMessage(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const [resetPassword, { data, loading }] = useMutation(RESET_PASSWORD); + const { + mutateAsync: resetPassword, + isPending, + isSuccess, + } = useResetPassword(); const token = searchParams.get('token'); const handleSubmit = async (values) => { - await resetPassword({ - variables: { - input: { - password: values.password, - token, + const { password } = values; + try { + await resetPassword({ + password, + token, + }); + enqueueSnackbar(formatMessage('resetPasswordForm.passwordUpdated'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-reset-password-success', }, - }, - }); - enqueueSnackbar(formatMessage('resetPasswordForm.passwordUpdated'), { - variant: 'success', - SnackbarProps: { - 'data-test': 'snackbar-reset-password-success', - }, - }); - navigate(URLS.LOGIN); + }); + navigate(URLS.LOGIN); + } catch (error) { + enqueueSnackbar( + error?.message || formatMessage('resetPasswordForm.error'), + { + variant: 'error', + }, + ); + } }; return ( @@ -113,8 +123,8 @@ export default function ResetPasswordForm() { variant="contained" color="primary" sx={{ boxShadow: 2, my: 3 }} - loading={loading} - disabled={data || !token} + loading={isPending} + disabled={isSuccess || !token} fullWidth > {formatMessage('resetPasswordForm.submit')} diff --git a/packages/web/src/components/SignUpForm/index.ee.jsx b/packages/web/src/components/SignUpForm/index.ee.jsx index d155729a..3ae6d2bc 100644 --- a/packages/web/src/components/SignUpForm/index.ee.jsx +++ b/packages/web/src/components/SignUpForm/index.ee.jsx @@ -11,8 +11,10 @@ import * as URLS from 'config/urls'; import { REGISTER_USER } from 'graphql/mutations/register-user.ee'; import Form from 'components/Form'; import TextField from 'components/TextField'; -import { LOGIN } from 'graphql/mutations/login'; import useFormatMessage from 'hooks/useFormatMessage'; +import useCreateAccessToken from 'hooks/useCreateAccessToken'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; + const validationSchema = yup.object().shape({ fullName: yup.string().trim().required('signupForm.mandatoryInput'), email: yup @@ -26,39 +28,57 @@ const validationSchema = yup.object().shape({ .required('signupForm.mandatoryInput') .oneOf([yup.ref('password')], 'signupForm.passwordsMustMatch'), }); + const initialValues = { fullName: '', email: '', password: '', confirmPassword: '', }; + function SignUpForm() { const navigate = useNavigate(); const authentication = useAuthentication(); const formatMessage = useFormatMessage(); + const enqueueSnackbar = useEnqueueSnackbar(); const [registerUser, { loading: registerUserLoading }] = useMutation(REGISTER_USER); - const [login, { loading: loginLoading }] = useMutation(LOGIN); + const { mutateAsync: createAccessToken, isPending: loginLoading } = + useCreateAccessToken(); + React.useEffect(() => { if (authentication.isAuthenticated) { navigate(URLS.DASHBOARD); } }, [authentication.isAuthenticated]); + const handleSubmit = async (values) => { const { fullName, email, password } = values; + await registerUser({ variables: { - input: { fullName, email, password }, + input: { + fullName, + email, + password, + }, }, }); - const { data } = await login({ - variables: { - input: { email, password }, - }, - }); - const { token } = data.login; - authentication.updateToken(token); + + try { + const { data } = await createAccessToken({ + email, + password, + }); + const { token } = data; + authentication.updateToken(token); + } catch (error) { + enqueueSnackbar(error?.message || formatMessage('signupForm.error'), { + variant: 'error', + }); + } }; + return ( ); } + export default SignUpForm; diff --git a/packages/web/src/graphql/mutations/delete-user.ee.js b/packages/web/src/graphql/mutations/delete-user.ee.js deleted file mode 100644 index c64916c3..00000000 --- a/packages/web/src/graphql/mutations/delete-user.ee.js +++ /dev/null @@ -1,6 +0,0 @@ -import { gql } from '@apollo/client'; -export const DELETE_USER = gql` - mutation DeleteUser($input: DeleteUserInput) { - deleteUser(input: $input) - } -`; diff --git a/packages/web/src/graphql/mutations/login.js b/packages/web/src/graphql/mutations/login.js deleted file mode 100644 index c0f632a5..00000000 --- a/packages/web/src/graphql/mutations/login.js +++ /dev/null @@ -1,12 +0,0 @@ -import { gql } from '@apollo/client'; -export const LOGIN = gql` - mutation Login($input: LoginInput) { - login(input: $input) { - token - user { - id - email - } - } - } -`; diff --git a/packages/web/src/graphql/mutations/reset-password.ee.js b/packages/web/src/graphql/mutations/reset-password.ee.js deleted file mode 100644 index 42360bca..00000000 --- a/packages/web/src/graphql/mutations/reset-password.ee.js +++ /dev/null @@ -1,6 +0,0 @@ -import { gql } from '@apollo/client'; -export const RESET_PASSWORD = gql` - mutation ResetPassword($input: ResetPasswordInput) { - resetPassword(input: $input) - } -`; diff --git a/packages/web/src/hooks/useAdminUserDelete.js b/packages/web/src/hooks/useAdminUserDelete.js new file mode 100644 index 00000000..94deb2ac --- /dev/null +++ b/packages/web/src/hooks/useAdminUserDelete.js @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useAdminUserDelete(userId) { + const mutation = useMutation({ + mutationFn: async () => { + const { data } = await api.delete(`/v1/admin/users/${userId}`); + + return data; + }, + }); + + return mutation; +} diff --git a/packages/web/src/hooks/useCreateAccessToken.js b/packages/web/src/hooks/useCreateAccessToken.js new file mode 100644 index 00000000..28b5e33d --- /dev/null +++ b/packages/web/src/hooks/useCreateAccessToken.js @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useCreateAccessToken() { + const query = useMutation({ + mutationFn: async ({ email, password }) => { + const { data } = await api.post('/v1/access-tokens', { email, password }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useResetPassword.js b/packages/web/src/hooks/useResetPassword.js new file mode 100644 index 00000000..48e8b5c2 --- /dev/null +++ b/packages/web/src/hooks/useResetPassword.js @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useResetPassword() { + const mutation = useMutation({ + mutationFn: async (payload) => { + const { data } = await api.post('/v1/users/reset-password', payload); + + return data; + }, + }); + + return mutation; +} diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index d2485902..e65d0ecd 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -144,6 +144,7 @@ "signupForm.validateEmail": "Email must be valid.", "signupForm.passwordsMustMatch": "Passwords must match.", "signupForm.mandatoryInput": "{inputName} is required.", + "signupForm.error": "Something went wrong. Please try again.", "loginForm.title": "Login", "loginForm.emailFieldLabel": "Email", "loginForm.passwordFieldLabel": "Password", @@ -152,6 +153,7 @@ "loginForm.noAccount": "Don't have an Automatisch account yet?", "loginForm.signUp": "Sign up", "loginPage.divider": "OR", + "loginForm.error": "Something went wrong. Please try again.", "ssoProviders.loginWithProvider": "Login with {providerName}", "forgotPasswordForm.title": "Forgot password", "forgotPasswordForm.submit": "Send reset instructions", @@ -165,6 +167,7 @@ "resetPasswordForm.passwordFieldLabel": "Password", "resetPasswordForm.confirmPasswordFieldLabel": "Confirm password", "resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login.", + "resetPasswordForm.error": "Something went wrong. Please try again.", "acceptInvitationForm.passwordsMustMatch": "Passwords must match.", "acceptInvitationForm.mandatoryInput": "{inputName} is required.", "acceptInvitationForm.title": "Accept invitation", @@ -210,6 +213,7 @@ "deleteUserButton.cancel": "Cancel", "deleteUserButton.confirm": "Delete", "deleteUserButton.successfullyDeleted": "The user has been deleted.", + "deleteUserButton.deleteError": "Failed while deleting!", "createUserPage.title": "Create user", "userForm.fullName": "Full name", "userForm.email": "Email", diff --git a/packages/web/src/routes.jsx b/packages/web/src/routes.jsx index 065dfeda..3c832d86 100644 --- a/packages/web/src/routes.jsx +++ b/packages/web/src/routes.jsx @@ -32,11 +32,11 @@ import useAuthentication from 'hooks/useAuthentication'; import Installation from 'pages/Installation'; function Routes() { - const { data: configData } = useAutomatischConfig(); + const { data: configData, isSuccess } = useAutomatischConfig(); const { isAuthenticated } = useAuthentication(); const config = configData?.data; - const installed = configData?.data?.['installation.completed'] === true; + const installed = isSuccess ? config?.['installation.completed'] === true : true; const navigate = useNavigate(); useEffect(() => {