Merge remote-tracking branch 'upstream/main' into AUT-1115

This commit is contained in:
Jakub P.
2024-07-22 16:32:09 +02:00
56 changed files with 500 additions and 252 deletions

View File

@@ -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
},
});
},
});

View File

@@ -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
},
});
},
});

View File

@@ -0,0 +1,4 @@
import createHmac from './create-hmac/index.js';
import createRsaSha256Signature from './create-rsa-sha256-signature/index.js';
export default [createHmac, createRsaSha256Signature];

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100pt" height="100pt" version="1.1" viewBox="0 0 100 100">
<path d="m66.012 33h-3.0117v-11c0-7.1719-5.8281-13-13-13s-13 5.8281-13 13v11h-3.0117c-2.75 0-4.9883 2.2383-4.9883 4.9883v28.012c0 2.75 2.2383 4.9883 4.9883 4.9883h32.012c2.75 0 4.9883-2.2383 4.9883-4.9883v-28.012c0.011719-2.75-2.2266-4.9883-4.9766-4.9883zm-27.012-11c0-6.0703 4.9297-11 11-11s11 4.9297 11 11v11h-22zm30 44.012c0 1.6484-1.3398 2.9883-2.9883 2.9883h-32.023c-1.6484 0-2.9883-1.3398-2.9883-2.9883v-28.023c0-1.6484 1.3398-2.9883 2.9883-2.9883h32.023c1.6484 0 2.9883 1.3398 2.9883 2.9883zm-18 9.9883v14c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-14c0-0.55078 0.44922-1 1-1s1 0.44922 1 1zm20 8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v7h7c0.55078 0 1 0.44922 1 1zm-32-8v8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h7v-7c0-0.55078 0.44922-1 1-1s1 0.44922 1 1zm-14-26c0 0.55078-0.44922 1-1 1h-14c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h14c0.55078 0 1 0.44922 1 1zm0-12c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v7h7c0.55078 0 1 0.44922 1 1zm0 24c0 0.55078-0.44922 1-1 1h-7v7c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1h8c0.55078 0 1 0.44922 1 1zm66-12c0 0.55078-0.44922 1-1 1h-14c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h14c0.55078 0 1 0.44922 1 1zm-16-12c0-0.55078 0.44922-1 1-1h7v-7c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1zm10 24v8c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-7h-7c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h8c0.55078 0 1 0.44922 1 1zm-35-17c-2.7617 0-5 2.2383-5 5 0 2.4102 1.7188 4.4297 4 4.8984v5.1016c0 0.55078 0.44922 1 1 1s1-0.44922 1-1v-5.1016c2.2812-0.46094 4-2.4805 4-4.8984 0-2.7617-2.2383-5-5-5zm0 8c-1.6484 0-3-1.3516-3-3s1.3516-3 3-3 3 1.3516 3 3-1.3516 3-3 3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -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,
});

View File

@@ -1,8 +1,10 @@
import defineAction from '../../../../helpers/define-action.js'; import defineAction from '../../../../helpers/define-action.js';
import formatDateTime from './transformers/format-date-time.js'; import formatDateTime from './transformers/format-date-time.js';
import getCurrentTimestamp from './transformers/get-current-timestamp.js';
const transformers = { const transformers = {
formatDateTime, formatDateTime,
getCurrentTimestamp,
}; };
export default defineAction({ export default defineAction({
@@ -16,7 +18,16 @@ export default defineAction({
type: 'dropdown', type: 'dropdown',
required: true, required: true,
variables: true, variables: true,
options: [{ label: 'Format Date / Time', value: 'formatDateTime' }], options: [
{
label: 'Get current timestamp',
value: 'getCurrentTimestamp',
},
{
label: 'Format Date / Time',
value: 'formatDateTime',
},
],
additionalFields: { additionalFields: {
type: 'query', type: 'query',
name: 'getDynamicFields', name: 'getDynamicFields',

View File

@@ -0,0 +1,5 @@
const getCurrentTimestamp = () => {
return Date.now();
};
export default getCurrentTimestamp;

View File

@@ -15,6 +15,7 @@ import encodeUri from './transformers/encode-uri.js';
import trimWhitespace from './transformers/trim-whitespace.js'; import trimWhitespace from './transformers/trim-whitespace.js';
import useDefaultValue from './transformers/use-default-value.js'; import useDefaultValue from './transformers/use-default-value.js';
import parseStringifiedJson from './transformers/parse-stringified-json.js'; import parseStringifiedJson from './transformers/parse-stringified-json.js';
import createUuid from './transformers/create-uuid.js';
const transformers = { const transformers = {
base64ToString, base64ToString,
@@ -32,6 +33,7 @@ const transformers = {
trimWhitespace, trimWhitespace,
useDefaultValue, useDefaultValue,
parseStringifiedJson, parseStringifiedJson,
createUuid,
}; };
export default defineAction({ export default defineAction({
@@ -49,22 +51,23 @@ export default defineAction({
options: [ options: [
{ label: 'Base64 to String', value: 'base64ToString' }, { label: 'Base64 to String', value: 'base64ToString' },
{ label: 'Capitalize', value: 'capitalize' }, { 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', label: 'Encode URI Component',
value: 'encodeUriComponent', value: 'encodeUriComponent',
}, },
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
{ label: 'Extract Email Address', value: 'extractEmailAddress' }, { label: 'Extract Email Address', value: 'extractEmailAddress' },
{ label: 'Extract Number', value: 'extractNumber' }, { label: 'Extract Number', value: 'extractNumber' },
{ label: 'Lowercase', value: 'lowercase' }, { label: 'Lowercase', value: 'lowercase' },
{ label: 'Parse stringified JSON', value: 'parseStringifiedJson' },
{ label: 'Pluralize', value: 'pluralize' }, { label: 'Pluralize', value: 'pluralize' },
{ label: 'Replace', value: 'replace' }, { label: 'Replace', value: 'replace' },
{ label: 'String to Base64', value: 'stringToBase64' }, { label: 'String to Base64', value: 'stringToBase64' },
{ label: 'Encode URI', value: 'encodeUri' },
{ label: 'Trim Whitespace', value: 'trimWhitespace' }, { label: 'Trim Whitespace', value: 'trimWhitespace' },
{ label: 'Use Default Value', value: 'useDefaultValue' }, { label: 'Use Default Value', value: 'useDefaultValue' },
{ label: 'Parse stringified JSON', value: 'parseStringifiedJson' },
], ],
additionalFields: { additionalFields: {
type: 'query', type: 'query',

View File

@@ -0,0 +1,7 @@
import { v4 as uuidv4 } from 'uuid';
const createUuidV4 = () => {
return uuidv4();
};
export default createUuidV4;

View File

@@ -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 upsertSamlAuthProvidersRoleMappings from './mutations/upsert-saml-auth-providers-role-mappings.ee.js';
import verifyConnection from './mutations/verify-connection.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 = { const mutationResolvers = {
createAppAuthClient, createAppAuthClient,
createAppConfig, createAppConfig,
@@ -47,14 +42,11 @@ const mutationResolvers = {
deleteFlow, deleteFlow,
deleteRole, deleteRole,
deleteStep, deleteStep,
deleteUser,
duplicateFlow, duplicateFlow,
executeFlow, executeFlow,
generateAuthUrl, generateAuthUrl,
login,
registerUser, registerUser,
resetConnection, resetConnection,
resetPassword,
updateAppAuthClient, updateAppAuthClient,
updateAppConfig, updateAppConfig,
updateConfig, updateConfig,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -14,14 +14,11 @@ type Mutation {
deleteFlow(input: DeleteFlowInput): Boolean deleteFlow(input: DeleteFlowInput): Boolean
deleteRole(input: DeleteRoleInput): Boolean deleteRole(input: DeleteRoleInput): Boolean
deleteStep(input: DeleteStepInput): Step deleteStep(input: DeleteStepInput): Step
deleteUser(input: DeleteUserInput): Boolean
duplicateFlow(input: DuplicateFlowInput): Flow duplicateFlow(input: DuplicateFlowInput): Flow
executeFlow(input: ExecuteFlowInput): executeFlowType executeFlow(input: ExecuteFlowInput): executeFlowType
generateAuthUrl(input: GenerateAuthUrlInput): AuthLink generateAuthUrl(input: GenerateAuthUrlInput): AuthLink
login(input: LoginInput): Auth
registerUser(input: RegisterUserInput): User registerUser(input: RegisterUserInput): User
resetConnection(input: ResetConnectionInput): Connection resetConnection(input: ResetConnectionInput): Connection
resetPassword(input: ResetPasswordInput): Boolean
updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient
updateAppConfig(input: UpdateAppConfigInput): AppConfig updateAppConfig(input: UpdateAppConfigInput): AppConfig
updateConfig(input: JSONObject): JSONObject updateConfig(input: JSONObject): JSONObject
@@ -153,11 +150,6 @@ enum ArgumentEnumType {
string string
} }
type Auth {
user: User
token: String
}
type AuthenticationStep { type AuthenticationStep {
type: String type: String
name: String name: String
@@ -388,10 +380,6 @@ input UpdateUserInput {
role: UserRoleInput role: UserRoleInput
} }
input DeleteUserInput {
id: String!
}
input RegisterUserInput { input RegisterUserInput {
fullName: String! fullName: String!
email: String! email: String!
@@ -404,16 +392,6 @@ input UpdateCurrentUserInput {
fullName: String fullName: String
} }
input ResetPasswordInput {
token: String!
password: String!
}
input LoginInput {
email: String!
password: String!
}
input PermissionInput { input PermissionInput {
action: String! action: String!
subject: String! subject: String!

View File

@@ -53,9 +53,7 @@ const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = { export const authenticationRules = {
Mutation: { Mutation: {
'*': isAuthenticatedRule, '*': isAuthenticatedRule,
login: allow,
registerUser: allow, registerUser: allow,
resetPassword: allow,
}, },
}; };

View File

@@ -59,6 +59,15 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/carbone/connection' }, { 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', text: 'Datastore',
collapsible: true, collapsible: true,

View File

@@ -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.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -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.

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100pt" height="100pt" version="1.1" viewBox="0 0 100 100">
<path d="m66.012 33h-3.0117v-11c0-7.1719-5.8281-13-13-13s-13 5.8281-13 13v11h-3.0117c-2.75 0-4.9883 2.2383-4.9883 4.9883v28.012c0 2.75 2.2383 4.9883 4.9883 4.9883h32.012c2.75 0 4.9883-2.2383 4.9883-4.9883v-28.012c0.011719-2.75-2.2266-4.9883-4.9766-4.9883zm-27.012-11c0-6.0703 4.9297-11 11-11s11 4.9297 11 11v11h-22zm30 44.012c0 1.6484-1.3398 2.9883-2.9883 2.9883h-32.023c-1.6484 0-2.9883-1.3398-2.9883-2.9883v-28.023c0-1.6484 1.3398-2.9883 2.9883-2.9883h32.023c1.6484 0 2.9883 1.3398 2.9883 2.9883zm-18 9.9883v14c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-14c0-0.55078 0.44922-1 1-1s1 0.44922 1 1zm20 8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v7h7c0.55078 0 1 0.44922 1 1zm-32-8v8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h7v-7c0-0.55078 0.44922-1 1-1s1 0.44922 1 1zm-14-26c0 0.55078-0.44922 1-1 1h-14c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h14c0.55078 0 1 0.44922 1 1zm0-12c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v7h7c0.55078 0 1 0.44922 1 1zm0 24c0 0.55078-0.44922 1-1 1h-7v7c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1h8c0.55078 0 1 0.44922 1 1zm66-12c0 0.55078-0.44922 1-1 1h-14c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h14c0.55078 0 1 0.44922 1 1zm-16-12c0-0.55078 0.44922-1 1-1h7v-7c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1zm10 24v8c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-7h-7c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h8c0.55078 0 1 0.44922 1 1zm-35-17c-2.7617 0-5 2.2383-5 5 0 2.4102 1.7188 4.4297 4 4.8984v5.1016c0 0.55078 0.44922 1 1 1s1-0.44922 1-1v-5.1016c2.2812-0.46094 4-2.4805 4-4.8984 0-2.7617-2.2383-5-5-5zm0 8c-1.6484 0-3-1.3516-3-3s1.3516-3 3-3 3 1.3516 3 3-1.3516 3-3 3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,6 @@
node_modules
build
.eslintrc.js
playwright-report/*

View File

@@ -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
]
}
}

View File

@@ -14,6 +14,6 @@ export class DeleteUserModal {
async close () { async close () {
await this.page.click('body', { await this.page.click('body', {
position: { x: 10, y: 10 } position: { x: 10, y: 10 }
}) });
} }
} }

View File

@@ -1,4 +1,4 @@
const { AdminCreateRolePage } = require('./create-role-page') const { AdminCreateRolePage } = require('./create-role-page');
export class AdminEditRolePage extends AdminCreateRolePage { export class AdminEditRolePage extends AdminCreateRolePage {
constructor (page) { constructor (page) {

View File

@@ -23,6 +23,7 @@ export class AdminEditUserPage extends AuthenticatedPage {
*/ */
async waitForLoad(fullName) { async waitForLoad(fullName) {
return await this.page.waitForFunction((fullName) => { return await this.page.waitForFunction((fullName) => {
// eslint-disable-next-line no-undef
const el = document.querySelector("[data-test='full-name-input']"); const el = document.querySelector("[data-test='full-name-input']");
return el && el.value === fullName; return el && el.value === fullName;
}, fullName); }, fullName);

View File

@@ -25,5 +25,5 @@ export const adminFixtures = {
adminCreateRolePage: async ({ page}, use) => { adminCreateRolePage: async ({ page}, use) => {
await use(new AdminCreateRolePage(page)); await use(new AdminCreateRolePage(page));
}, },
} };

View File

@@ -87,6 +87,7 @@ export class AdminUsersPage extends AuthenticatedPage {
await this.firstPageButton.click(); await this.firstPageButton.click();
} }
// eslint-disable-next-line no-constant-condition
while (true) { while (true) {
if (await this.usersLoader.isVisible()) { if (await this.usersLoader.isVisible()) {
await this.usersLoader.waitFor({ await this.usersLoader.waitFor({
@@ -108,6 +109,7 @@ export class AdminUsersPage extends AuthenticatedPage {
async getTotalRows() { async getTotalRows() {
return await this.page.evaluate(() => { return await this.page.evaluate(() => {
// eslint-disable-next-line no-undef
const node = document.querySelector('[data-total-count]'); const node = document.querySelector('[data-total-count]');
if (node) { if (node) {
const count = Number(node.dataset.totalCount); const count = Number(node.dataset.totalCount);
@@ -121,6 +123,7 @@ export class AdminUsersPage extends AuthenticatedPage {
async getRowsPerPage() { async getRowsPerPage() {
return await this.page.evaluate(() => { return await this.page.evaluate(() => {
// eslint-disable-next-line no-undef
const node = document.querySelector('[data-rows-per-page]'); const node = document.querySelector('[data-rows-per-page]');
if (node) { if (node) {
const count = Number(node.dataset.rowsPerPage); const count = Number(node.dataset.rowsPerPage);

View File

@@ -25,7 +25,7 @@ export class ApplicationsModal extends BasePage {
if (this.applications[link] === undefined) { if (this.applications[link] === undefined) {
throw { throw {
message: `Unknown link "${link}" passed to ApplicationsModal.selectLink` message: `Unknown link "${link}" passed to ApplicationsModal.selectLink`
} };
} }
await this.searchInput.fill(link); await this.searchInput.fill(link);
await this.appListItem.first().click(); await this.appListItem.first().click();

View File

@@ -1,10 +1,11 @@
const { BasePage } = require('../../base-page'); const { BasePage } = require('../../base-page');
const { AddGithubConnectionModal } = require('./add-github-connection-modal'); const { AddGithubConnectionModal } = require('./add-github-connection-modal');
const { expect } = require('@playwright/test');
export class GithubPage extends BasePage { export class GithubPage extends BasePage {
constructor (page) { constructor (page) {
super(page) super(page);
this.addConnectionButton = page.getByTestId('add-connection-button'); this.addConnectionButton = page.getByTestId('add-connection-button');
this.connectionsTab = page.getByTestId('connections-tab'); this.connectionsTab = page.getByTestId('connections-tab');
this.flowsTab = page.getByTestId('flows-tab'); this.flowsTab = page.getByTestId('flows-tab');
@@ -38,7 +39,7 @@ export class GithubPage extends BasePage {
await this.flowsTab.click(); await this.flowsTab.click();
await expect(this.flowsTab).toBeVisible(); await expect(this.flowsTab).toBeVisible();
} }
return await this.flowRows.count() > 0 return await this.flowRows.count() > 0;
} }
async hasConnections () { async hasConnections () {

View File

@@ -1,4 +1,5 @@
const { BasePage } = require('../../base-page'); const { BasePage } = require('../../base-page');
const { expect } = require('@playwright/test');
export class GithubPopup extends BasePage { export class GithubPopup extends BasePage {
@@ -11,7 +12,7 @@ export class GithubPopup extends BasePage {
} }
getPathname () { getPathname () {
const url = this.page.url() const url = this.page.url();
try { try {
return new URL(url).pathname; return new URL(url).pathname;
} catch (e) { } catch (e) {
@@ -34,17 +35,17 @@ export class GithubPopup extends BasePage {
loginInput.click(); loginInput.click();
await loginInput.fill(process.env.GITHUB_USERNAME); await loginInput.fill(process.env.GITHUB_USERNAME);
const passwordInput = this.page.getByLabel('Password'); const passwordInput = this.page.getByLabel('Password');
passwordInput.click() passwordInput.click();
await passwordInput.fill(process.env.GITHUB_PASSWORD); await passwordInput.fill(process.env.GITHUB_PASSWORD);
await this.page.getByRole('button', { name: 'Sign in' }).click(); await this.page.getByRole('button', { name: 'Sign in' }).click();
// await this.page.waitForTimeout(2000); // await this.page.waitForTimeout(2000);
if (this.page.isClosed()) { if (this.page.isClosed()) {
return return;
} }
// await this.page.waitForLoadState('networkidle', 30000); // await this.page.waitForLoadState('networkidle', 30000);
this.page.waitForEvent('load'); this.page.waitForEvent('load');
if (this.page.isClosed()) { if (this.page.isClosed()) {
return return;
} }
await this.page.waitForURL(function (url) { await this.page.waitForURL(function (url) {
const u = new URL(url); const u = new URL(url);
@@ -55,7 +56,7 @@ export class GithubPopup extends BasePage {
} }
async handleAuthorize () { async handleAuthorize () {
if (this.page.isClosed()) { return } if (this.page.isClosed()) { return; }
const authorizeButton = this.page.getByRole( const authorizeButton = this.page.getByRole(
'button', 'button',
{ name: 'Authorize' } { name: 'Authorize' }
@@ -69,7 +70,7 @@ export class GithubPopup extends BasePage {
) && ( ) && (
u.searchParams.get('client_id') === null u.searchParams.get('client_id') === null
); );
}) });
const passwordInput = this.page.getByLabel('Password'); const passwordInput = this.page.getByLabel('Password');
if (await passwordInput.isVisible()) { if (await passwordInput.isVisible()) {
await passwordInput.fill(process.env.GITHUB_PASSWORD); 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');
} }
} }

View File

@@ -1,7 +1,4 @@
const path = require('node:path');
const { expect } = require('@playwright/test');
const { BasePage } = require('./base-page'); const { BasePage } = require('./base-page');
const { LoginPage } = require('./login-page');
export class AuthenticatedPage extends BasePage { export class AuthenticatedPage extends BasePage {
/** /**

View File

@@ -1,4 +1,3 @@
const path = require('node:path');
const { AuthenticatedPage } = require('./authenticated-page'); const { AuthenticatedPage } = require('./authenticated-page');
export class ConnectionsPage extends AuthenticatedPage { export class ConnectionsPage extends AuthenticatedPage {

View File

@@ -1,4 +1,3 @@
const path = require('node:path');
const { AuthenticatedPage } = require('./authenticated-page'); const { AuthenticatedPage } = require('./authenticated-page');
export class ExecutionsPage extends AuthenticatedPage { export class ExecutionsPage extends AuthenticatedPage {

View File

@@ -1,4 +1,3 @@
const path = require('node:path');
const { AuthenticatedPage } = require('./authenticated-page'); const { AuthenticatedPage } = require('./authenticated-page');
export class FlowEditorPage extends AuthenticatedPage { export class FlowEditorPage extends AuthenticatedPage {

View File

@@ -8,6 +8,7 @@ const { LoginPage } = require('./login-page');
const { AcceptInvitation } = require('./accept-invitation-page'); const { AcceptInvitation } = require('./accept-invitation-page');
const { adminFixtures } = require('./admin'); const { adminFixtures } = require('./admin');
const { AdminSetupPage } = require('./admin-setup-page'); const { AdminSetupPage } = require('./admin-setup-page');
const { AdminCreateUserPage } = require('./admin/create-user-page');
exports.test = test.extend({ exports.test = test.extend({
page: async ({ page }, use) => { page: async ({ page }, use) => {
@@ -58,6 +59,11 @@ exports.publicTest = test.extend({
const adminSetupPage = new AdminSetupPage(page); const adminSetupPage = new AdminSetupPage(page);
await use(adminSetupPage); await use(adminSetupPage);
}, },
adminCreateUserPage: async ({page}, use) => {
const adminCreateUserPage = new AdminCreateUserPage(page);
await use(adminCreateUserPage);
}
}); });
expect.extend({ expect.extend({

View File

@@ -1,11 +1,11 @@
const { Client } = require('pg'); const { Client } = require('pg');
const client = new Client({ const client = new Client({
host: process.env.POSTGRES_HOST, host: process.env.POSTGRES_HOST,
user: process.env.POSTGRES_USERNAME, user: process.env.POSTGRES_USERNAME,
port: process.env.POSTGRES_PORT, port: process.env.POSTGRES_PORT,
password: process.env.POSTGRES_PASSWORD, password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DATABASE database: process.env.POSTGRES_DATABASE
}); });
exports.client = client; exports.client = client;

View File

@@ -1,4 +1,3 @@
const path = require('node:path');
const { AuthenticatedPage } = require('./authenticated-page'); const { AuthenticatedPage } = require('./authenticated-page');
export class UserInterfacePage extends AuthenticatedPage { export class UserInterfacePage extends AuthenticatedPage {

View File

@@ -7,7 +7,8 @@
"scripts": { "scripts": {
"start-mock-license-server": "node ./license-server-with-mock.js", "start-mock-license-server": "node ./license-server-with-mock.js",
"test": "playwright test", "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": [ "contributors": [
{ {

View File

@@ -17,7 +17,6 @@ test.describe('Role management page', () => {
adminCreateRolePage, adminCreateRolePage,
adminEditRolePage, adminEditRolePage,
adminRolesPage, adminRolesPage,
page,
}) => { }) => {
await test.step('Create a new role', async () => { await test.step('Create a new role', async () => {
await adminRolesPage.navigateTo(); await adminRolesPage.navigateTo();
@@ -126,12 +125,14 @@ test.describe('Role management page', () => {
await adminCreateRolePage.isMounted(); await adminCreateRolePage.isMounted();
const initScrollTop = await page.evaluate(() => { const initScrollTop = await page.evaluate(() => {
// eslint-disable-next-line no-undef
return document.documentElement.scrollTop; return document.documentElement.scrollTop;
}); });
await page.mouse.move(400, 100); await page.mouse.move(400, 100);
await page.mouse.click(400, 100); await page.mouse.click(400, 100);
await page.mouse.wheel(200, 0); await page.mouse.wheel(200, 0);
const updatedScrollTop = await page.evaluate(() => { const updatedScrollTop = await page.evaluate(() => {
// eslint-disable-next-line no-undef
return document.documentElement.scrollTop; return document.documentElement.scrollTop;
}); });
await expect(initScrollTop).not.toBe(updatedScrollTop); await expect(initScrollTop).not.toBe(updatedScrollTop);
@@ -144,11 +145,13 @@ test.describe('Role management page', () => {
await adminEditRolePage.isMounted(); await adminEditRolePage.isMounted();
const initScrollTop = await page.evaluate(() => { const initScrollTop = await page.evaluate(() => {
// eslint-disable-next-line no-undef
return document.documentElement.scrollTop; return document.documentElement.scrollTop;
}); });
await page.mouse.move(400, 100); await page.mouse.move(400, 100);
await page.mouse.wheel(200, 0); await page.mouse.wheel(200, 0);
const updatedScrollTop = await page.evaluate(() => { const updatedScrollTop = await page.evaluate(() => {
// eslint-disable-next-line no-undef
return document.documentElement.scrollTop; return document.documentElement.scrollTop;
}); });
await expect(initScrollTop).not.toBe(updatedScrollTop); await expect(initScrollTop).not.toBe(updatedScrollTop);
@@ -165,7 +168,6 @@ test.describe('Role management page', () => {
adminUsersPage, adminUsersPage,
adminCreateUserPage, adminCreateUserPage,
adminEditUserPage, adminEditUserPage,
page,
}) => { }) => {
await adminRolesPage.navigateTo(); await adminRolesPage.navigateTo();
await test.step('Create a new role', async () => { await test.step('Create a new role', async () => {
@@ -270,7 +272,6 @@ test.describe('Role management page', () => {
adminRolesPage, adminRolesPage,
adminUsersPage, adminUsersPage,
adminCreateUserPage, adminCreateUserPage,
page,
}) => { }) => {
await adminRolesPage.navigateTo(); await adminRolesPage.navigateTo();
await test.step('Create a new role', async () => { 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.goto(url);
await page.waitForTimeout(750); await page.waitForTimeout(750);
const isUnmounted = await page.evaluate(() => { const isUnmounted = await page.evaluate(() => {
// eslint-disable-next-line no-undef
const root = document.querySelector('#root'); const root = document.querySelector('#root');
if (root) { if (root) {

View File

@@ -98,7 +98,7 @@ test.describe('User management page', () => {
await expect(userRow).not.toBeVisible(false); await expect(userRow).not.toBeVisible(false);
} }
); );
}); });
test( test(
'Creating a user which has been deleted', 'Creating a user which has been deleted',

View File

@@ -51,7 +51,7 @@ test(
const subjects = ['Connection', 'Execution', 'Flow']; const subjects = ['Connection', 'Execution', 'Flow'];
for (let subject of subjects) { for (let subject of subjects) {
const row = adminCreateRolePage.getSubjectRow(subject) const row = adminCreateRolePage.getSubjectRow(subject);
const modal = adminCreateRolePage.getRoleConditionsModal(subject); const modal = adminCreateRolePage.getRoleConditionsModal(subject);
await adminCreateRolePage.clickPermissionSettings(row); await adminCreateRolePage.clickPermissionSettings(row);
await expect(modal.modal).toBeVisible(); await expect(modal.modal).toBeVisible();

View File

@@ -2,7 +2,7 @@ const { test, expect } = require('../../fixtures/index');
// no execution data exists in an empty account // no execution data exists in an empty account
test.describe.skip('Executions page', () => { 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('executions-page-drawer-link').click();
await page.getByTestId('execution-row').first().click(); await page.getByTestId('execution-row').first().click();

View File

@@ -6,7 +6,7 @@ test('Ensure creating a new flow works', async ({ page }) => {
await expect(page).toHaveURL( 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}/ /\/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( test(
'Create a new flow with a Scheduler step then an Ntfy step', 'Create a new flow with a Scheduler step then an Ntfy step',

View File

@@ -1,17 +1,8 @@
const { AdminCreateUserPage } = require('../../fixtures/admin/create-user-page');
const { publicTest, expect } = require('../../fixtures/index'); const { publicTest, expect } = require('../../fixtures/index');
const { client } = require('../../fixtures/postgres-client-config'); const { client } = require('../../fixtures/postgres-client-config');
const { DateTime } = require('luxon'); const { DateTime } = require('luxon');
publicTest.describe('Accept invitation page', () => { 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 }) => { publicTest('should not be able to set the password if token is empty', async ({ acceptInvitationPage }) => {
await acceptInvitationPage.open(''); await acceptInvitationPage.open('');
await acceptInvitationPage.excpectSubmitButtonToBeDisabled(); await acceptInvitationPage.excpectSubmitButtonToBeDisabled();
@@ -19,44 +10,83 @@ publicTest.describe('Accept invitation page', () => {
await acceptInvitationPage.excpectSubmitButtonToBeDisabled(); 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 }) => { publicTest('should not be able to set the password if token is not in db', async ({ acceptInvitationPage }) => {
await acceptInvitationPage.open('abc'); await acceptInvitationPage.open('abc');
await acceptInvitationPage.acceptInvitation('something'); await acceptInvitationPage.acceptInvitation('something');
await acceptInvitationPage.expectAlertToBeVisible(); 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();
});
});
}); });

View File

@@ -10,7 +10,7 @@ import CardActionArea from '@mui/material/CardActionArea';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useQueryClient } from '@tanstack/react-query';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import ConnectionContextMenu from 'components/AppConnectionContextMenu'; import ConnectionContextMenu from 'components/AppConnectionContextMenu';
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection'; import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
@@ -35,6 +35,7 @@ function AppConnectionRow(props) {
const [verificationVisible, setVerificationVisible] = React.useState(false); const [verificationVisible, setVerificationVisible] = React.useState(false);
const contextButtonRef = React.useRef(null); const contextButtonRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null); const [anchorEl, setAnchorEl] = React.useState(null);
const queryClient = useQueryClient();
const [deleteConnection] = useMutation(DELETE_CONNECTION); const [deleteConnection] = useMutation(DELETE_CONNECTION);
@@ -75,6 +76,9 @@ function AppConnectionRow(props) {
}, },
}); });
await queryClient.invalidateQueries({
queryKey: ['apps', key, 'connections'],
});
enqueueSnackbar(formatMessage('connection.deletedMessage'), { enqueueSnackbar(formatMessage('connection.deletedMessage'), {
variant: 'success', variant: 'success',
SnackbarProps: { SnackbarProps: {
@@ -86,7 +90,7 @@ function AppConnectionRow(props) {
testConnection({ variables: { id } }); testConnection({ variables: { id } });
} }
}, },
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar], [deleteConnection, id, queryClient, key, enqueueSnackbar, formatMessage, testConnection],
); );
const relativeCreatedAt = DateTime.fromMillis( const relativeCreatedAt = DateTime.fromMillis(

View File

@@ -1,5 +1,4 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useMutation } from '@apollo/client';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
@@ -7,16 +6,14 @@ import { useQueryClient } from '@tanstack/react-query';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import * as React from 'react'; import * as React from 'react';
import ConfirmationDialog from 'components/ConfirmationDialog'; import ConfirmationDialog from 'components/ConfirmationDialog';
import { DELETE_USER } from 'graphql/mutations/delete-user.ee';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useAdminUserDelete from 'hooks/useAdminUserDelete';
function DeleteUserButton(props) { function DeleteUserButton(props) {
const { userId } = props; const { userId } = props;
const [showConfirmation, setShowConfirmation] = React.useState(false); const [showConfirmation, setShowConfirmation] = React.useState(false);
const [deleteUser] = useMutation(DELETE_USER, { const { mutateAsync: deleteUser } = useAdminUserDelete(userId);
variables: { input: { id: userId } },
refetchQueries: ['GetUsers'],
});
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const enqueueSnackbar = useEnqueueSnackbar(); const enqueueSnackbar = useEnqueueSnackbar();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -33,7 +30,12 @@ function DeleteUserButton(props) {
}, },
}); });
} catch (error) { } catch (error) {
throw new Error('Failed while deleting!'); enqueueSnackbar(
error?.message || formatMessage('deleteUserButton.deleteError'),
{
variant: 'error',
},
);
} }
}, [deleteUser]); }, [deleteUser]);

View File

@@ -1,6 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useNavigate, Link as RouterLink } from 'react-router-dom'; import { useNavigate, Link as RouterLink } from 'react-router-dom';
import { useMutation } from '@apollo/client';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
@@ -8,17 +7,20 @@ import LoadingButton from '@mui/lab/LoadingButton';
import useAuthentication from 'hooks/useAuthentication'; import useAuthentication from 'hooks/useAuthentication';
import useCloud from 'hooks/useCloud'; import useCloud from 'hooks/useCloud';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import { LOGIN } from 'graphql/mutations/login';
import Form from 'components/Form'; import Form from 'components/Form';
import TextField from 'components/TextField'; import TextField from 'components/TextField';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useCreateAccessToken from 'hooks/useCreateAccessToken';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
function LoginForm() { function LoginForm() {
const isCloud = useCloud(); const isCloud = useCloud();
const navigate = useNavigate(); const navigate = useNavigate();
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const enqueueSnackbar = useEnqueueSnackbar();
const authentication = useAuthentication(); const authentication = useAuthentication();
const [login, { loading }] = useMutation(LOGIN); const { mutateAsync: createAccessToken, isPending: loading } =
useCreateAccessToken();
React.useEffect(() => { React.useEffect(() => {
if (authentication.isAuthenticated) { if (authentication.isAuthenticated) {
@@ -27,13 +29,19 @@ function LoginForm() {
}, [authentication.isAuthenticated]); }, [authentication.isAuthenticated]);
const handleSubmit = async (values) => { const handleSubmit = async (values) => {
const { data } = await login({ try {
variables: { const { email, password } = values;
input: values, const { data } = await createAccessToken({
}, email,
}); password,
const { token } = data.login; });
authentication.updateToken(token); const { token } = data;
authentication.updateToken(token);
} catch (error) {
enqueueSnackbar(error?.message || formatMessage('loginForm.error'), {
variant: 'error',
});
}
}; };
return ( return (

View File

@@ -1,4 +1,3 @@
import { useMutation } from '@apollo/client';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
@@ -7,11 +6,12 @@ import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import * as React from 'react'; import * as React from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import * as yup from 'yup'; import * as yup from 'yup';
import Form from 'components/Form'; import Form from 'components/Form';
import TextField from 'components/TextField'; import TextField from 'components/TextField';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import { RESET_PASSWORD } from 'graphql/mutations/reset-password.ee';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useResetPassword from 'hooks/useResetPassword';
const validationSchema = yup.object().shape({ const validationSchema = yup.object().shape({
password: yup.string().required('resetPasswordForm.mandatoryInput'), password: yup.string().required('resetPasswordForm.mandatoryInput'),
@@ -26,25 +26,35 @@ export default function ResetPasswordForm() {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [resetPassword, { data, loading }] = useMutation(RESET_PASSWORD); const {
mutateAsync: resetPassword,
isPending,
isSuccess,
} = useResetPassword();
const token = searchParams.get('token'); const token = searchParams.get('token');
const handleSubmit = async (values) => { const handleSubmit = async (values) => {
await resetPassword({ const { password } = values;
variables: { try {
input: { await resetPassword({
password: values.password, password,
token, token,
});
enqueueSnackbar(formatMessage('resetPasswordForm.passwordUpdated'), {
variant: 'success',
SnackbarProps: {
'data-test': 'snackbar-reset-password-success',
}, },
}, });
}); navigate(URLS.LOGIN);
enqueueSnackbar(formatMessage('resetPasswordForm.passwordUpdated'), { } catch (error) {
variant: 'success', enqueueSnackbar(
SnackbarProps: { error?.message || formatMessage('resetPasswordForm.error'),
'data-test': 'snackbar-reset-password-success', {
}, variant: 'error',
}); },
navigate(URLS.LOGIN); );
}
}; };
return ( return (
@@ -113,8 +123,8 @@ export default function ResetPasswordForm() {
variant="contained" variant="contained"
color="primary" color="primary"
sx={{ boxShadow: 2, my: 3 }} sx={{ boxShadow: 2, my: 3 }}
loading={loading} loading={isPending}
disabled={data || !token} disabled={isSuccess || !token}
fullWidth fullWidth
> >
{formatMessage('resetPasswordForm.submit')} {formatMessage('resetPasswordForm.submit')}

View File

@@ -11,8 +11,10 @@ import * as URLS from 'config/urls';
import { REGISTER_USER } from 'graphql/mutations/register-user.ee'; import { REGISTER_USER } from 'graphql/mutations/register-user.ee';
import Form from 'components/Form'; import Form from 'components/Form';
import TextField from 'components/TextField'; import TextField from 'components/TextField';
import { LOGIN } from 'graphql/mutations/login';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useCreateAccessToken from 'hooks/useCreateAccessToken';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
const validationSchema = yup.object().shape({ const validationSchema = yup.object().shape({
fullName: yup.string().trim().required('signupForm.mandatoryInput'), fullName: yup.string().trim().required('signupForm.mandatoryInput'),
email: yup email: yup
@@ -26,39 +28,57 @@ const validationSchema = yup.object().shape({
.required('signupForm.mandatoryInput') .required('signupForm.mandatoryInput')
.oneOf([yup.ref('password')], 'signupForm.passwordsMustMatch'), .oneOf([yup.ref('password')], 'signupForm.passwordsMustMatch'),
}); });
const initialValues = { const initialValues = {
fullName: '', fullName: '',
email: '', email: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
}; };
function SignUpForm() { function SignUpForm() {
const navigate = useNavigate(); const navigate = useNavigate();
const authentication = useAuthentication(); const authentication = useAuthentication();
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const enqueueSnackbar = useEnqueueSnackbar();
const [registerUser, { loading: registerUserLoading }] = const [registerUser, { loading: registerUserLoading }] =
useMutation(REGISTER_USER); useMutation(REGISTER_USER);
const [login, { loading: loginLoading }] = useMutation(LOGIN); const { mutateAsync: createAccessToken, isPending: loginLoading } =
useCreateAccessToken();
React.useEffect(() => { React.useEffect(() => {
if (authentication.isAuthenticated) { if (authentication.isAuthenticated) {
navigate(URLS.DASHBOARD); navigate(URLS.DASHBOARD);
} }
}, [authentication.isAuthenticated]); }, [authentication.isAuthenticated]);
const handleSubmit = async (values) => { const handleSubmit = async (values) => {
const { fullName, email, password } = values; const { fullName, email, password } = values;
await registerUser({ await registerUser({
variables: { variables: {
input: { fullName, email, password }, input: {
fullName,
email,
password,
},
}, },
}); });
const { data } = await login({
variables: { try {
input: { email, password }, const { data } = await createAccessToken({
}, email,
}); password,
const { token } = data.login; });
authentication.updateToken(token); const { token } = data;
authentication.updateToken(token);
} catch (error) {
enqueueSnackbar(error?.message || formatMessage('signupForm.error'), {
variant: 'error',
});
}
}; };
return ( return (
<Paper sx={{ px: 2, py: 4 }}> <Paper sx={{ px: 2, py: 4 }}>
<Typography <Typography
@@ -168,4 +188,5 @@ function SignUpForm() {
</Paper> </Paper>
); );
} }
export default SignUpForm; export default SignUpForm;

View File

@@ -1,6 +0,0 @@
import { gql } from '@apollo/client';
export const DELETE_USER = gql`
mutation DeleteUser($input: DeleteUserInput) {
deleteUser(input: $input)
}
`;

View File

@@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
export const LOGIN = gql`
mutation Login($input: LoginInput) {
login(input: $input) {
token
user {
id
email
}
}
}
`;

View File

@@ -1,6 +0,0 @@
import { gql } from '@apollo/client';
export const RESET_PASSWORD = gql`
mutation ResetPassword($input: ResetPasswordInput) {
resetPassword(input: $input)
}
`;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -144,6 +144,7 @@
"signupForm.validateEmail": "Email must be valid.", "signupForm.validateEmail": "Email must be valid.",
"signupForm.passwordsMustMatch": "Passwords must match.", "signupForm.passwordsMustMatch": "Passwords must match.",
"signupForm.mandatoryInput": "{inputName} is required.", "signupForm.mandatoryInput": "{inputName} is required.",
"signupForm.error": "Something went wrong. Please try again.",
"loginForm.title": "Login", "loginForm.title": "Login",
"loginForm.emailFieldLabel": "Email", "loginForm.emailFieldLabel": "Email",
"loginForm.passwordFieldLabel": "Password", "loginForm.passwordFieldLabel": "Password",
@@ -152,6 +153,7 @@
"loginForm.noAccount": "Don't have an Automatisch account yet?", "loginForm.noAccount": "Don't have an Automatisch account yet?",
"loginForm.signUp": "Sign up", "loginForm.signUp": "Sign up",
"loginPage.divider": "OR", "loginPage.divider": "OR",
"loginForm.error": "Something went wrong. Please try again.",
"ssoProviders.loginWithProvider": "Login with {providerName}", "ssoProviders.loginWithProvider": "Login with {providerName}",
"forgotPasswordForm.title": "Forgot password", "forgotPasswordForm.title": "Forgot password",
"forgotPasswordForm.submit": "Send reset instructions", "forgotPasswordForm.submit": "Send reset instructions",
@@ -165,6 +167,7 @@
"resetPasswordForm.passwordFieldLabel": "Password", "resetPasswordForm.passwordFieldLabel": "Password",
"resetPasswordForm.confirmPasswordFieldLabel": "Confirm password", "resetPasswordForm.confirmPasswordFieldLabel": "Confirm password",
"resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login.", "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.passwordsMustMatch": "Passwords must match.",
"acceptInvitationForm.mandatoryInput": "{inputName} is required.", "acceptInvitationForm.mandatoryInput": "{inputName} is required.",
"acceptInvitationForm.title": "Accept invitation", "acceptInvitationForm.title": "Accept invitation",
@@ -210,6 +213,7 @@
"deleteUserButton.cancel": "Cancel", "deleteUserButton.cancel": "Cancel",
"deleteUserButton.confirm": "Delete", "deleteUserButton.confirm": "Delete",
"deleteUserButton.successfullyDeleted": "The user has been deleted.", "deleteUserButton.successfullyDeleted": "The user has been deleted.",
"deleteUserButton.deleteError": "Failed while deleting!",
"createUserPage.title": "Create user", "createUserPage.title": "Create user",
"userForm.fullName": "Full name", "userForm.fullName": "Full name",
"userForm.email": "Email", "userForm.email": "Email",

View File

@@ -32,11 +32,11 @@ import useAuthentication from 'hooks/useAuthentication';
import Installation from 'pages/Installation'; import Installation from 'pages/Installation';
function Routes() { function Routes() {
const { data: configData } = useAutomatischConfig(); const { data: configData, isSuccess } = useAutomatischConfig();
const { isAuthenticated } = useAuthentication(); const { isAuthenticated } = useAuthentication();
const config = configData?.data; const config = configData?.data;
const installed = configData?.data?.['installation.completed'] === true; const installed = isSuccess ? config?.['installation.completed'] === true : true;
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {