Merge remote-tracking branch 'upstream/main' into AUT-1115
This commit is contained in:
@@ -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
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
@@ -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
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
4
packages/backend/src/apps/cryptography/actions/index.js
Normal file
4
packages/backend/src/apps/cryptography/actions/index.js
Normal 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];
|
@@ -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 |
14
packages/backend/src/apps/cryptography/index.js
Normal file
14
packages/backend/src/apps/cryptography/index.js
Normal 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,
|
||||
});
|
@@ -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',
|
||||
|
@@ -0,0 +1,5 @@
|
||||
const getCurrentTimestamp = () => {
|
||||
return Date.now();
|
||||
};
|
||||
|
||||
export default getCurrentTimestamp;
|
@@ -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',
|
||||
|
@@ -0,0 +1,7 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const createUuidV4 = () => {
|
||||
return uuidv4();
|
||||
};
|
||||
|
||||
export default createUuidV4;
|
@@ -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,
|
||||
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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!
|
||||
|
@@ -53,9 +53,7 @@ const isAuthenticatedRule = rule()(isAuthenticated);
|
||||
export const authenticationRules = {
|
||||
Mutation: {
|
||||
'*': isAuthenticatedRule,
|
||||
login: allow,
|
||||
registerUser: allow,
|
||||
resetPassword: allow,
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -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,
|
||||
|
14
packages/docs/pages/apps/cryptography/actions.md
Normal file
14
packages/docs/pages/apps/cryptography/actions.md
Normal 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 />
|
3
packages/docs/pages/apps/cryptography/connection.md
Normal file
3
packages/docs/pages/apps/cryptography/connection.md
Normal 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.
|
3
packages/docs/pages/public/favicons/cryptography.svg
Normal file
3
packages/docs/pages/public/favicons/cryptography.svg
Normal 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 |
6
packages/e2e-tests/.eslintignore
Normal file
6
packages/e2e-tests/.eslintignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
build
|
||||
|
||||
.eslintrc.js
|
||||
|
||||
playwright-report/*
|
25
packages/e2e-tests/.eslintrc.json
Normal file
25
packages/e2e-tests/.eslintrc.json
Normal 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
|
||||
]
|
||||
}
|
||||
}
|
@@ -14,6 +14,6 @@ export class DeleteUserModal {
|
||||
async close () {
|
||||
await this.page.click('body', {
|
||||
position: { x: 10, y: 10 }
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
const { AdminCreateRolePage } = require('./create-role-page')
|
||||
const { AdminCreateRolePage } = require('./create-role-page');
|
||||
|
||||
export class AdminEditRolePage extends AdminCreateRolePage {
|
||||
constructor (page) {
|
||||
|
@@ -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);
|
||||
|
@@ -25,5 +25,5 @@ export const adminFixtures = {
|
||||
adminCreateRolePage: async ({ page}, use) => {
|
||||
await use(new AdminCreateRolePage(page));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
|
@@ -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 () {
|
||||
|
@@ -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');
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
/**
|
||||
|
@@ -1,4 +1,3 @@
|
||||
const path = require('node:path');
|
||||
const { AuthenticatedPage } = require('./authenticated-page');
|
||||
|
||||
export class ConnectionsPage extends AuthenticatedPage {
|
||||
|
@@ -1,4 +1,3 @@
|
||||
const path = require('node:path');
|
||||
const { AuthenticatedPage } = require('./authenticated-page');
|
||||
|
||||
export class ExecutionsPage extends AuthenticatedPage {
|
||||
|
@@ -1,4 +1,3 @@
|
||||
const path = require('node:path');
|
||||
const { AuthenticatedPage } = require('./authenticated-page');
|
||||
|
||||
export class FlowEditorPage extends AuthenticatedPage {
|
||||
|
@@ -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({
|
||||
|
@@ -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;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
const path = require('node:path');
|
||||
const { AuthenticatedPage } = require('./authenticated-page');
|
||||
|
||||
export class UserInterfacePage extends AuthenticatedPage {
|
||||
|
@@ -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": [
|
||||
{
|
||||
|
@@ -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) {
|
||||
|
@@ -98,7 +98,7 @@ test.describe('User management page', () => {
|
||||
await expect(userRow).not.toBeVisible(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'Creating a user which has been deleted',
|
||||
|
@@ -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();
|
||||
|
@@ -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();
|
||||
|
||||
|
@@ -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',
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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(
|
||||
|
@@ -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]);
|
||||
|
||||
|
@@ -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 (
|
||||
|
@@ -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')}
|
||||
|
@@ -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 (
|
||||
<Paper sx={{ px: 2, py: 4 }}>
|
||||
<Typography
|
||||
@@ -168,4 +188,5 @@ function SignUpForm() {
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignUpForm;
|
||||
|
@@ -1,6 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
export const DELETE_USER = gql`
|
||||
mutation DeleteUser($input: DeleteUserInput) {
|
||||
deleteUser(input: $input)
|
||||
}
|
||||
`;
|
@@ -1,12 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
export const LOGIN = gql`
|
||||
mutation Login($input: LoginInput) {
|
||||
login(input: $input) {
|
||||
token
|
||||
user {
|
||||
id
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@@ -1,6 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
export const RESET_PASSWORD = gql`
|
||||
mutation ResetPassword($input: ResetPasswordInput) {
|
||||
resetPassword(input: $input)
|
||||
}
|
||||
`;
|
15
packages/web/src/hooks/useAdminUserDelete.js
Normal file
15
packages/web/src/hooks/useAdminUserDelete.js
Normal 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;
|
||||
}
|
15
packages/web/src/hooks/useCreateAccessToken.js
Normal file
15
packages/web/src/hooks/useCreateAccessToken.js
Normal 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;
|
||||
}
|
15
packages/web/src/hooks/useResetPassword.js
Normal file
15
packages/web/src/hooks/useResetPassword.js
Normal 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;
|
||||
}
|
@@ -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",
|
||||
|
@@ -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(() => {
|
||||
|
Reference in New Issue
Block a user