Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a650e3beaa | ||
![]() |
dba0041e5f | ||
![]() |
b8a44afd25 | ||
![]() |
e2445bf585 | ||
![]() |
50706c524e | ||
![]() |
11e0cb9398 | ||
![]() |
1e82e40802 | ||
![]() |
ff00644e62 | ||
![]() |
97bcd3792b | ||
![]() |
5738a09771 | ||
![]() |
c461cc4878 | ||
![]() |
878fab347a | ||
![]() |
354b331b08 | ||
![]() |
3b9aadb90f | ||
![]() |
7193d018ce | ||
![]() |
d5cea034ac | ||
![]() |
a2760c10b3 | ||
![]() |
3593cf3808 |
@@ -38,7 +38,7 @@
|
|||||||
"@types/xmlrpc": "^1.3.7",
|
"@types/xmlrpc": "^1.3.7",
|
||||||
"accounting": "^0.4.1",
|
"accounting": "^0.4.1",
|
||||||
"ajv-formats": "^2.1.1",
|
"ajv-formats": "^2.1.1",
|
||||||
"axios": "0.24.0",
|
"axios": "1.6.0",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"bullmq": "^3.0.0",
|
"bullmq": "^3.0.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
|
34
packages/backend/src/apps/amazon-s3/assets/favicon.svg
Normal file
34
packages/backend/src/apps/amazon-s3/assets/favicon.svg
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="428" height="512" viewBox="0 0 428 512">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #e25444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-1, .cls-2, .cls-3 {
|
||||||
|
fill-rule: evenodd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #7b1d13;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #58150d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-1" d="M378,99L295,257l83,158,34-19V118Z"/>
|
||||||
|
<path class="cls-2" d="M378,99L212,118,127.5,257,212,396l166,19V99Z"/>
|
||||||
|
<path class="cls-3" d="M43,99L16,111V403l27,12L212,257Z"/>
|
||||||
|
<path class="cls-1" d="M42.637,98.667l169.587,47.111V372.444L42.637,415.111V98.667Z"/>
|
||||||
|
<path class="cls-3" d="M212.313,170.667l-72.008-11.556,72.008-81.778,71.83,81.778Z"/>
|
||||||
|
<path class="cls-3" d="M284.143,159.111l-71.919,11.733-71.919-11.733V77.333"/>
|
||||||
|
<path class="cls-3" d="M212.313,342.222l-72.008,13.334,72.008,70.222,71.83-70.222Z"/>
|
||||||
|
<path class="cls-2" d="M212,16L140,54V159l72.224-20.333Z"/>
|
||||||
|
<path class="cls-2" d="M212.224,196.444l-71.919,7.823V309.105l71.919,8.228V196.444Z"/>
|
||||||
|
<path class="cls-2" d="M212.224,373.333L140.305,355.3V458.363L212.224,496V373.333Z"/>
|
||||||
|
<path class="cls-1" d="M284.143,355.3l-71.919,18.038V496l71.919-37.637V355.3Z"/>
|
||||||
|
<path class="cls-1" d="M212.224,196.444l71.919,7.823V309.105l-71.919,8.228V196.444Z"/>
|
||||||
|
<path class="cls-1" d="M212,16l72,38V159l-72-20V16Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
56
packages/backend/src/apps/amazon-s3/auth/index.ts
Normal file
56
packages/backend/src/apps/amazon-s3/auth/index.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import verifyCredentials from './verify-credentials';
|
||||||
|
import isStillVerified from './is-still-verified';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'oAuthRedirectUrl',
|
||||||
|
label: 'OAuth Redirect URL',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: true,
|
||||||
|
value: '{WEB_APP_URL}/app/amazon-s3/connections/add',
|
||||||
|
placeholder: null,
|
||||||
|
description:
|
||||||
|
'When asked to input a redirect URL in AWS, enter the URL above.',
|
||||||
|
clickToCopy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'accessKeyId',
|
||||||
|
label: 'Access Key ID',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: null,
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'secretAccessKey',
|
||||||
|
label: 'Secret Access Key',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: null,
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'screenName',
|
||||||
|
label: 'Screen Name',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description:
|
||||||
|
'Screen name of your connection to be used on Automatisch UI.',
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
verifyCredentials,
|
||||||
|
isStillVerified,
|
||||||
|
};
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
import getCurrentUser from '../common/get-current-user';
|
||||||
|
|
||||||
|
const isStillVerified = async ($: IGlobalVariable) => {
|
||||||
|
const currentUser = await getCurrentUser($);
|
||||||
|
return !!currentUser.resourceName;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isStillVerified;
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
|
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||||
|
const { data } = await $.http.get('/');
|
||||||
|
|
||||||
|
console.log('data:', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verifyCredentials;
|
153
packages/backend/src/apps/amazon-s3/common/add-auth-header.ts
Normal file
153
packages/backend/src/apps/amazon-s3/common/add-auth-header.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { IJSONObject, TBeforeRequest } from '@automatisch/types';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { getISODate, getYYYYMMDD } from './get-current-date';
|
||||||
|
|
||||||
|
function hmac(key: string | Buffer, data: string) {
|
||||||
|
return crypto.createHmac('sha256', key).update(data).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hmacWoHex(key: Buffer | string, data: string) {
|
||||||
|
return crypto.createHmac('sha256', key).update(data).digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hash(data: string) {
|
||||||
|
return crypto.createHash('sha256').update(data).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareCanonicalRequest(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
queryParams: IJSONObject | string,
|
||||||
|
headers: IJSONObject,
|
||||||
|
payload: string
|
||||||
|
) {
|
||||||
|
const canonicalRequest = [method, encodeURIComponent(path)];
|
||||||
|
|
||||||
|
// Step 3: Canonical Query String
|
||||||
|
if (typeof queryParams === 'string') {
|
||||||
|
canonicalRequest.push('');
|
||||||
|
} else {
|
||||||
|
const sortedQueryParams = Object.keys(queryParams)
|
||||||
|
.map(
|
||||||
|
(key) =>
|
||||||
|
`${encodeURIComponent(key)}=${encodeURIComponent(
|
||||||
|
queryParams[key] as string
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
.sort();
|
||||||
|
canonicalRequest.push(sortedQueryParams.join('&'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Canonical Headers
|
||||||
|
const sortedHeaders = Object.keys(headers)
|
||||||
|
.sort()
|
||||||
|
.map((key) => `${key.toLowerCase()}:${(headers[key] as string).trim()}`);
|
||||||
|
|
||||||
|
canonicalRequest.push(sortedHeaders.join('\n'));
|
||||||
|
|
||||||
|
// Step 5: Signed Headers
|
||||||
|
const signedHeaders = Object.keys(headers)
|
||||||
|
.sort()
|
||||||
|
.map((key) => key.toLowerCase())
|
||||||
|
.join(';');
|
||||||
|
canonicalRequest.push(signedHeaders);
|
||||||
|
|
||||||
|
const hashedPayload = hash(payload);
|
||||||
|
canonicalRequest.push(hashedPayload);
|
||||||
|
|
||||||
|
return canonicalRequest.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareStringToSign(
|
||||||
|
datetime: string,
|
||||||
|
credentialScope: string,
|
||||||
|
hashedCanonicalRequest: string
|
||||||
|
) {
|
||||||
|
const stringToSign = [
|
||||||
|
'AWS4-HMAC-SHA256',
|
||||||
|
datetime,
|
||||||
|
credentialScope,
|
||||||
|
hashedCanonicalRequest,
|
||||||
|
];
|
||||||
|
|
||||||
|
return stringToSign.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateSigningKey(
|
||||||
|
secretKey: string,
|
||||||
|
date: string,
|
||||||
|
region: string,
|
||||||
|
service: string
|
||||||
|
) {
|
||||||
|
const dateKey = hmacWoHex('AWS4' + secretKey, date);
|
||||||
|
const dateRegionKey = hmacWoHex(dateKey, region);
|
||||||
|
const dateRegionServiceKey = hmacWoHex(dateRegionKey, service);
|
||||||
|
const signingKey = hmacWoHex(dateRegionServiceKey, 'aws4_request');
|
||||||
|
return signingKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAuthorizationHeader(
|
||||||
|
accessKey: string,
|
||||||
|
credentialScope: string,
|
||||||
|
signedHeaders: string,
|
||||||
|
signature: string
|
||||||
|
) {
|
||||||
|
return `AWS4-HMAC-SHA256 Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||||
|
const accessKeyId = $.auth.data.accessKeyId as string;
|
||||||
|
const secretAccessKey = $.auth.data.secretAccessKey as string;
|
||||||
|
const date = getYYYYMMDD();
|
||||||
|
const formattedDate = getISODate();
|
||||||
|
const region = 'us-east-1';
|
||||||
|
const method = 'GET';
|
||||||
|
const path = '/';
|
||||||
|
const queryParams = '';
|
||||||
|
const payload = '';
|
||||||
|
const headers = {
|
||||||
|
Host: 's3.amazonaws.com',
|
||||||
|
'X-Amz-Content-Sha256': hash(payload),
|
||||||
|
'X-Amz-Date': formattedDate,
|
||||||
|
};
|
||||||
|
const headerKeys = Object.keys(headers)
|
||||||
|
.sort()
|
||||||
|
.map((header) => header.toLowerCase())
|
||||||
|
.join(';');
|
||||||
|
|
||||||
|
const canonicalRequest = prepareCanonicalRequest(
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
queryParams,
|
||||||
|
headers,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
const stringToSign = prepareStringToSign(
|
||||||
|
formattedDate,
|
||||||
|
`${date}/${region}/s3/aws4_request`,
|
||||||
|
hash(canonicalRequest)
|
||||||
|
);
|
||||||
|
|
||||||
|
const signingKey = calculateSigningKey(secretAccessKey, date, region, 's3');
|
||||||
|
|
||||||
|
const signature = hmac(signingKey, stringToSign);
|
||||||
|
|
||||||
|
const authorizationHeader = createAuthorizationHeader(
|
||||||
|
accessKeyId,
|
||||||
|
`${date}/${region}/s3/aws4_request`,
|
||||||
|
headerKeys,
|
||||||
|
signature
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($.auth.data?.secretAccessKey && $.auth.data?.accessKeyId) {
|
||||||
|
requestConfig.headers.Authorization = authorizationHeader;
|
||||||
|
requestConfig.headers['Host'] = 's3.amazonaws.com';
|
||||||
|
requestConfig.headers['X-Amz-Content-Sha256'] = hash(payload);
|
||||||
|
requestConfig.headers['X-Amz-Date'] = formattedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default addAuthHeader;
|
@@ -0,0 +1,13 @@
|
|||||||
|
export const getYYYYMMDD = () => {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
const month = (today.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const day = today.getDate().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
const formattedDate = `${year}${month}${day}`;
|
||||||
|
return formattedDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getISODate = () => {
|
||||||
|
return new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
|
||||||
|
};
|
@@ -1,9 +1,7 @@
|
|||||||
import { IGlobalVariable } from '@automatisch/types';
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
const getCurrentUser = async ($: IGlobalVariable) => {
|
const getCurrentUser = async ($: IGlobalVariable) => {
|
||||||
const { data: currentUser } = await $.http.get(
|
const { data: currentUser } = await $.http.get('/');
|
||||||
'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses'
|
|
||||||
);
|
|
||||||
return currentUser;
|
return currentUser;
|
||||||
};
|
};
|
||||||
|
|
0
packages/backend/src/apps/amazon-s3/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/amazon-s3/index.d.ts
vendored
Normal file
16
packages/backend/src/apps/amazon-s3/index.ts
Normal file
16
packages/backend/src/apps/amazon-s3/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import defineApp from '../../helpers/define-app';
|
||||||
|
import addAuthHeader from './common/add-auth-header';
|
||||||
|
import auth from './auth';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'Amazon S3',
|
||||||
|
key: 'amazon-s3',
|
||||||
|
baseUrl: '',
|
||||||
|
apiBaseUrl: 'https://s3.amazonaws.com',
|
||||||
|
iconUrl: '{BASE_URL}/apps/amazon-s3/assets/favicon.svg',
|
||||||
|
authDocUrl: 'https://automatisch.io/docs/apps/amazon-s3/connection',
|
||||||
|
primaryColor: '7B1D13',
|
||||||
|
supportsConnections: true,
|
||||||
|
beforeRequest: [addAuthHeader],
|
||||||
|
auth,
|
||||||
|
});
|
@@ -1,5 +1,5 @@
|
|||||||
import { IGlobalVariable } from '@automatisch/types';
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
import getCurrentUser from '../common/get-current-user';
|
import getCurrentUser from '../../amazon-s3/common/get-current-user';
|
||||||
|
|
||||||
const isStillVerified = async ($: IGlobalVariable) => {
|
const isStillVerified = async ($: IGlobalVariable) => {
|
||||||
const currentUser = await getCurrentUser($);
|
const currentUser = await getCurrentUser($);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { IField, IGlobalVariable } from '@automatisch/types';
|
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||||
import getCurrentUser from '../common/get-current-user';
|
import getCurrentUser from '../../amazon-s3/common/get-current-user';
|
||||||
|
|
||||||
type TUser = {
|
type TUser = {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
@@ -0,0 +1,53 @@
|
|||||||
|
import defineAction from '../../../../helpers/define-action';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Create link post',
|
||||||
|
key: 'createLinkPost',
|
||||||
|
description: 'Create a new link post within a subreddit.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Title',
|
||||||
|
key: 'title',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
description:
|
||||||
|
'Heading for the recent post. Limited to 300 characters or less.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Subreddit',
|
||||||
|
key: 'subreddit',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
description: 'The subreddit for posting. Note: Exclude /r/.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Url',
|
||||||
|
key: 'url',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const { title, subreddit, url } = $.step.parameters;
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
kind: 'link',
|
||||||
|
api_type: 'json',
|
||||||
|
title: title as string,
|
||||||
|
sr: subreddit as string,
|
||||||
|
url: url as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await $.http.post('/api/submit', params.toString());
|
||||||
|
|
||||||
|
$.setActionItem({
|
||||||
|
raw: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
3
packages/backend/src/apps/reddit/actions/index.ts
Normal file
3
packages/backend/src/apps/reddit/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import createLinkPost from './create-link-post';
|
||||||
|
|
||||||
|
export default [createLinkPost];
|
1
packages/backend/src/apps/reddit/assets/favicon.svg
Normal file
1
packages/backend/src/apps/reddit/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="_1O4jTk-dZ-VIxsCuYB6OR8 " width="40" height="48" ><g><circle fill="#FF4500" cx="10" cy="10" r="10"></circle><path fill="#FFFFFF" d="M16.67,10A1.46,1.46,0,0,0,14.2,9a7.12,7.12,0,0,0-3.85-1.23L11,4.65,13.14,5.1a1,1,0,1,0,.13-0.61L10.82,4a0.31,0.31,0,0,0-.37.24L9.71,7.71a7.14,7.14,0,0,0-3.9,1.23A1.46,1.46,0,1,0,4.2,11.33a2.87,2.87,0,0,0,0,.44c0,2.24,2.61,4.06,5.83,4.06s5.83-1.82,5.83-4.06a2.87,2.87,0,0,0,0-.44A1.46,1.46,0,0,0,16.67,10Zm-10,1a1,1,0,1,1,1,1A1,1,0,0,1,6.67,11Zm5.81,2.75a3.84,3.84,0,0,1-2.47.77,3.84,3.84,0,0,1-2.47-.77,0.27,0.27,0,0,1,.38-0.38A3.27,3.27,0,0,0,10,14a3.28,3.28,0,0,0,2.09-.61A0.27,0.27,0,1,1,12.48,13.79Zm-0.18-1.71a1,1,0,1,1,1-1A1,1,0,0,1,12.29,12.08Z"></path></g></svg>
|
After Width: | Height: | Size: 813 B |
26
packages/backend/src/apps/reddit/auth/generate-auth-url.ts
Normal file
26
packages/backend/src/apps/reddit/auth/generate-auth-url.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
import authScope from '../common/auth-scope';
|
||||||
|
|
||||||
|
export default async function generateAuthUrl($: IGlobalVariable) {
|
||||||
|
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||||
|
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||||
|
);
|
||||||
|
const redirectUri = oauthRedirectUrlField.value as string;
|
||||||
|
const state = Math.random().toString() as string;
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
client_id: $.auth.data.clientId as string,
|
||||||
|
response_type: 'code',
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
duration: 'permanent',
|
||||||
|
scope: authScope.join(' '),
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `https://www.reddit.com/api/v1/authorize?${searchParams.toString()}`;
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
url,
|
||||||
|
originalState: state,
|
||||||
|
});
|
||||||
|
}
|
48
packages/backend/src/apps/reddit/auth/index.ts
Normal file
48
packages/backend/src/apps/reddit/auth/index.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import generateAuthUrl from './generate-auth-url';
|
||||||
|
import verifyCredentials from './verify-credentials';
|
||||||
|
import refreshToken from './refresh-token';
|
||||||
|
import isStillVerified from './is-still-verified';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'oAuthRedirectUrl',
|
||||||
|
label: 'OAuth Redirect URL',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: true,
|
||||||
|
value: '{WEB_APP_URL}/app/reddit/connections/add',
|
||||||
|
placeholder: null,
|
||||||
|
description:
|
||||||
|
'When asked to input a redirect URL in Reddit, enter the URL above.',
|
||||||
|
clickToCopy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clientId',
|
||||||
|
label: 'Client ID',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: null,
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clientSecret',
|
||||||
|
label: 'Client Secret',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: null,
|
||||||
|
clickToCopy: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
generateAuthUrl,
|
||||||
|
verifyCredentials,
|
||||||
|
isStillVerified,
|
||||||
|
refreshToken,
|
||||||
|
};
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
import getCurrentUser from '../common/get-current-user';
|
||||||
|
|
||||||
|
const isStillVerified = async ($: IGlobalVariable) => {
|
||||||
|
const currentUser = await getCurrentUser($);
|
||||||
|
return !!currentUser.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isStillVerified;
|
29
packages/backend/src/apps/reddit/auth/refresh-token.ts
Normal file
29
packages/backend/src/apps/reddit/auth/refresh-token.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { URLSearchParams } from 'node:url';
|
||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
|
const refreshToken = async ($: IGlobalVariable) => {
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
$.auth.data.clientId + ':' + $.auth.data.clientSecret
|
||||||
|
).toString('base64')}`,
|
||||||
|
};
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: $.auth.data.refreshToken as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await $.http.post(
|
||||||
|
'https://www.reddit.com/api/v1/access_token',
|
||||||
|
params.toString(),
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
accessToken: data.access_token,
|
||||||
|
expiresIn: data.expires_in,
|
||||||
|
scope: data.scope,
|
||||||
|
tokenType: data.token_type,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default refreshToken;
|
48
packages/backend/src/apps/reddit/auth/verify-credentials.ts
Normal file
48
packages/backend/src/apps/reddit/auth/verify-credentials.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||||
|
import getCurrentUser from '../common/get-current-user';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
|
||||||
|
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||||
|
if ($.auth.data.originalState !== $.auth.data.state) {
|
||||||
|
throw new Error(`The 'state' parameter does not match.`);
|
||||||
|
}
|
||||||
|
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||||
|
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||||
|
);
|
||||||
|
const redirectUri = oauthRedirectUrlField.value as string;
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
$.auth.data.clientId + ':' + $.auth.data.clientSecret
|
||||||
|
).toString('base64')}`,
|
||||||
|
};
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: $.auth.data.code as string,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await $.http.post(
|
||||||
|
'https://www.reddit.com/api/v1/access_token',
|
||||||
|
params.toString(),
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
accessToken: data.access_token,
|
||||||
|
tokenType: data.token_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUser = await getCurrentUser($);
|
||||||
|
const screenName = currentUser?.name;
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
clientId: $.auth.data.clientId,
|
||||||
|
clientSecret: $.auth.data.clientSecret,
|
||||||
|
scope: $.auth.data.scope,
|
||||||
|
expiresIn: data.expires_in,
|
||||||
|
refreshToken: data.refresh_token,
|
||||||
|
screenName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verifyCredentials;
|
11
packages/backend/src/apps/reddit/common/add-auth-header.ts
Normal file
11
packages/backend/src/apps/reddit/common/add-auth-header.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { TBeforeRequest } from '@automatisch/types';
|
||||||
|
|
||||||
|
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||||
|
if ($.auth.data?.accessToken) {
|
||||||
|
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default addAuthHeader;
|
3
packages/backend/src/apps/reddit/common/auth-scope.ts
Normal file
3
packages/backend/src/apps/reddit/common/auth-scope.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const authScope: string[] = ['identity', 'read', 'account', 'submit'];
|
||||||
|
|
||||||
|
export default authScope;
|
@@ -0,0 +1,8 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
|
const getCurrentUser = async ($: IGlobalVariable) => {
|
||||||
|
const { data: currentUser } = await $.http.get('/api/v1/me');
|
||||||
|
return currentUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getCurrentUser;
|
0
packages/backend/src/apps/reddit/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/reddit/index.d.ts
vendored
Normal file
20
packages/backend/src/apps/reddit/index.ts
Normal file
20
packages/backend/src/apps/reddit/index.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import defineApp from '../../helpers/define-app';
|
||||||
|
import addAuthHeader from './common/add-auth-header';
|
||||||
|
import auth from './auth';
|
||||||
|
import triggers from './triggers';
|
||||||
|
import actions from './actions';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'Reddit',
|
||||||
|
key: 'reddit',
|
||||||
|
baseUrl: 'https://www.reddit.com',
|
||||||
|
apiBaseUrl: 'https://oauth.reddit.com',
|
||||||
|
iconUrl: '{BASE_URL}/apps/reddit/assets/favicon.svg',
|
||||||
|
authDocUrl: 'https://automatisch.io/docs/apps/reddit/connection',
|
||||||
|
primaryColor: 'FF4500',
|
||||||
|
supportsConnections: true,
|
||||||
|
beforeRequest: [addAuthHeader],
|
||||||
|
auth,
|
||||||
|
triggers,
|
||||||
|
actions,
|
||||||
|
});
|
3
packages/backend/src/apps/reddit/triggers/index.ts
Normal file
3
packages/backend/src/apps/reddit/triggers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import newPostsMatchingSearch from './new-posts-matching-search';
|
||||||
|
|
||||||
|
export default [newPostsMatchingSearch];
|
@@ -0,0 +1,48 @@
|
|||||||
|
import defineTrigger from '../../../../helpers/define-trigger';
|
||||||
|
|
||||||
|
export default defineTrigger({
|
||||||
|
name: 'New posts matching search',
|
||||||
|
key: 'newPostsMatchingSearch',
|
||||||
|
pollInterval: 15,
|
||||||
|
description: 'Triggers when a search string matches a new post.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Search Query',
|
||||||
|
key: 'searchQuery',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
description:
|
||||||
|
'The term or expression to look for, restricted to 512 characters. If your query contains periods (e.g., automatisch.io), ensure it is enclosed in quotes ("automatisch.io").',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const { searchQuery } = $.step.parameters;
|
||||||
|
const params = {
|
||||||
|
q: searchQuery,
|
||||||
|
type: 'link',
|
||||||
|
sort: 'new',
|
||||||
|
limit: 100,
|
||||||
|
after: undefined as unknown as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get('/search', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
params.after = data.data.after;
|
||||||
|
|
||||||
|
if (data.data.children?.length) {
|
||||||
|
for (const item of data.data.children) {
|
||||||
|
$.pushTriggerItem({
|
||||||
|
raw: item,
|
||||||
|
meta: {
|
||||||
|
internalId: item.data.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (params.after);
|
||||||
|
},
|
||||||
|
});
|
@@ -1,3 +1,4 @@
|
|||||||
import newBankTransactions from './new-bank-transactions';
|
import newBankTransactions from './new-bank-transactions';
|
||||||
|
import newPayments from './new-payments';
|
||||||
|
|
||||||
export default [newBankTransactions];
|
export default [newBankTransactions, newPayments];
|
||||||
|
109
packages/backend/src/apps/xero/triggers/new-payments/index.ts
Normal file
109
packages/backend/src/apps/xero/triggers/new-payments/index.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import defineTrigger from '../../../../helpers/define-trigger';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
page: number;
|
||||||
|
order: string;
|
||||||
|
where?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineTrigger({
|
||||||
|
name: 'New payments',
|
||||||
|
key: 'newPayments',
|
||||||
|
pollInterval: 15,
|
||||||
|
description: 'Triggers when a new payment is received.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Organization',
|
||||||
|
key: 'organizationId',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: true,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
source: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicData',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listOrganizations',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Payment Type',
|
||||||
|
key: 'paymentType',
|
||||||
|
type: 'dropdown' as const,
|
||||||
|
required: false,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
value: '',
|
||||||
|
options: [
|
||||||
|
{ label: 'Accounts Receivable', value: 'ACCRECPAYMENT' },
|
||||||
|
{ label: 'Accounts Payable', value: 'ACCPAYPAYMENT' },
|
||||||
|
{
|
||||||
|
label: 'Accounts Receivable Credit (Refund)',
|
||||||
|
value: 'ARCREDITPAYMENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Payable Credit (Refund)',
|
||||||
|
value: 'APCREDITPAYMENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Receivable Overpayment (Refund)',
|
||||||
|
value: 'AROVERPAYMENTPAYMENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Receivable Prepayment (Refund)',
|
||||||
|
value: 'ARPREPAYMENTPAYMENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Payable Prepayment (Refund)',
|
||||||
|
value: 'APPREPAYMENTPAYMENT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Payable Overpayment (Refund)',
|
||||||
|
value: 'APOVERPAYMENTPAYMENT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const paymentType = $.step.parameters.paymentType;
|
||||||
|
|
||||||
|
const params: Params = {
|
||||||
|
page: 1,
|
||||||
|
order: 'Date DESC',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (paymentType) {
|
||||||
|
params.where = `PaymentType="${paymentType}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextPage = false;
|
||||||
|
do {
|
||||||
|
const { data } = await $.http.get('/api.xro/2.0/Payments', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
params.page = params.page + 1;
|
||||||
|
|
||||||
|
if (data.Payments?.length) {
|
||||||
|
for (const payment of data.Payments) {
|
||||||
|
$.pushTriggerItem({
|
||||||
|
raw: payment,
|
||||||
|
meta: {
|
||||||
|
internalId: payment.PaymentID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.Payments?.length === 100) {
|
||||||
|
nextPage = true;
|
||||||
|
} else {
|
||||||
|
nextPage = false;
|
||||||
|
}
|
||||||
|
} while (nextPage);
|
||||||
|
},
|
||||||
|
});
|
@@ -1,5 +1,5 @@
|
|||||||
import { IHttpClientParams } from '@automatisch/types';
|
import { IHttpClientParams } from '@automatisch/types';
|
||||||
import { AxiosRequestConfig } from 'axios';
|
import { InternalAxiosRequestConfig } from 'axios';
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
export { AxiosInstance as IHttpClient } from 'axios';
|
export { AxiosInstance as IHttpClient } from 'axios';
|
||||||
|
|
||||||
@@ -7,8 +7,8 @@ import HttpError from '../../errors/http';
|
|||||||
import axios from '../axios-with-proxy';
|
import axios from '../axios-with-proxy';
|
||||||
|
|
||||||
const removeBaseUrlForAbsoluteUrls = (
|
const removeBaseUrlForAbsoluteUrls = (
|
||||||
requestConfig: AxiosRequestConfig
|
requestConfig: InternalAxiosRequestConfig
|
||||||
): AxiosRequestConfig => {
|
): InternalAxiosRequestConfig => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(requestConfig.url);
|
const url = new URL(requestConfig.url);
|
||||||
requestConfig.baseURL = url.origin;
|
requestConfig.baseURL = url.origin;
|
||||||
@@ -30,12 +30,21 @@ export default function createHttpClient({
|
|||||||
});
|
});
|
||||||
|
|
||||||
instance.interceptors.request.use(
|
instance.interceptors.request.use(
|
||||||
(requestConfig: AxiosRequestConfig): AxiosRequestConfig => {
|
(requestConfig: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
||||||
const newRequestConfig = removeBaseUrlForAbsoluteUrls(requestConfig);
|
const newRequestConfig = removeBaseUrlForAbsoluteUrls(requestConfig);
|
||||||
|
|
||||||
return beforeRequest.reduce((newConfig, beforeRequestFunc) => {
|
const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => {
|
||||||
return beforeRequestFunc($, newConfig);
|
return beforeRequestFunc($, newConfig);
|
||||||
}, newRequestConfig);
|
}, newRequestConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* axios seems to want InternalAxiosRequestConfig returned not AxioRequestConfig
|
||||||
|
* anymore even though requests do require AxiosRequestConfig.
|
||||||
|
*
|
||||||
|
* Since both interfaces are very similar (InternalAxiosRequestConfig
|
||||||
|
* extends AxiosRequestConfig), we can utilize an assertion below
|
||||||
|
**/
|
||||||
|
return result as InternalAxiosRequestConfig;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -289,6 +289,16 @@ export default defineConfig({
|
|||||||
{ text: 'Connection', link: '/apps/pushover/connection' },
|
{ text: 'Connection', link: '/apps/pushover/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Reddit',
|
||||||
|
collapsible: true,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Triggers', link: '/apps/reddit/triggers' },
|
||||||
|
{ text: 'Actions', link: '/apps/reddit/actions' },
|
||||||
|
{ text: 'Connection', link: '/apps/reddit/connection' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Remove.bg',
|
text: 'Remove.bg',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
@@ -453,6 +463,15 @@ export default defineConfig({
|
|||||||
{ text: 'Connection', link: '/apps/wordpress/connection' },
|
{ text: 'Connection', link: '/apps/wordpress/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Xero',
|
||||||
|
collapsible: true,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Triggers', link: '/apps/xero/triggers' },
|
||||||
|
{ text: 'Connection', link: '/apps/xero/connection' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Youtube',
|
text: 'Youtube',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
|
12
packages/docs/pages/apps/reddit/actions.md
Normal file
12
packages/docs/pages/apps/reddit/actions.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
favicon: /favicons/reddit.svg
|
||||||
|
items:
|
||||||
|
- name: Create link post
|
||||||
|
desc: Create a new link post within a subreddit.
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomListing from '../../components/CustomListing.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CustomListing />
|
15
packages/docs/pages/apps/reddit/connection.md
Normal file
15
packages/docs/pages/apps/reddit/connection.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Reddit
|
||||||
|
|
||||||
|
:::info
|
||||||
|
This page explains the steps you need to follow to set up the Reddit
|
||||||
|
connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||||
|
:::
|
||||||
|
|
||||||
|
1. Go to [Reddit apps page](https://www.reddit.com/prefs/apps).
|
||||||
|
2. Click on the **"are you a developer? create an app..."** button in order to create an app.
|
||||||
|
3. Fill the **Name** field and choose **web app**.
|
||||||
|
4. Copy **OAuth Redirect URL** from Automatisch to **redirect uri** field.
|
||||||
|
5. Click on the **create app** button.
|
||||||
|
6. Copy the client id below **web app** text to the `Client ID` field on Automatisch.
|
||||||
|
7. Copy the **secret** value to the `Client Secret` field on Automatisch.
|
||||||
|
8. Start using Reddit integration with Automatisch!
|
12
packages/docs/pages/apps/reddit/triggers.md
Normal file
12
packages/docs/pages/apps/reddit/triggers.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
favicon: /favicons/reddit.svg
|
||||||
|
items:
|
||||||
|
- name: New posts matching search
|
||||||
|
desc: Triggers when a search string matches a new post.
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomListing from '../../components/CustomListing.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CustomListing />
|
@@ -5,6 +5,8 @@ items:
|
|||||||
desc: Creates an attachment of a specified object by given parent ID.
|
desc: Creates an attachment of a specified object by given parent ID.
|
||||||
- name: Find record
|
- name: Find record
|
||||||
desc: Finds a record of a specified object by a field and value.
|
desc: Finds a record of a specified object by a field and value.
|
||||||
|
- name: Execute query
|
||||||
|
desc: Executes a SOQL query in Salesforce.
|
||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
@@ -3,6 +3,8 @@ favicon: /favicons/xero.svg
|
|||||||
items:
|
items:
|
||||||
- name: New bank transactions
|
- name: New bank transactions
|
||||||
desc: Triggers when a new bank transaction occurs.
|
desc: Triggers when a new bank transaction occurs.
|
||||||
|
- name: New payments
|
||||||
|
desc: Triggers when a new payment is received.
|
||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
@@ -30,6 +30,7 @@ The following integrations are currently supported by Automatisch.
|
|||||||
- [Placetel](/apps/placetel/triggers)
|
- [Placetel](/apps/placetel/triggers)
|
||||||
- [PostgreSQL](/apps/postgresql/actions)
|
- [PostgreSQL](/apps/postgresql/actions)
|
||||||
- [Pushover](/apps/pushover/actions)
|
- [Pushover](/apps/pushover/actions)
|
||||||
|
- [Reddit](/apps/reddit/triggers)
|
||||||
- [Remove.bg](/apps/removebg/connection)
|
- [Remove.bg](/apps/removebg/connection)
|
||||||
- [RSS](/apps/rss/triggers)
|
- [RSS](/apps/rss/triggers)
|
||||||
- [Salesforce](/apps/salesforce/triggers)
|
- [Salesforce](/apps/salesforce/triggers)
|
||||||
@@ -48,5 +49,6 @@ The following integrations are currently supported by Automatisch.
|
|||||||
- [Typeform](/apps/typeform/triggers)
|
- [Typeform](/apps/typeform/triggers)
|
||||||
- [Webhooks](/apps/webhooks/triggers)
|
- [Webhooks](/apps/webhooks/triggers)
|
||||||
- [WordPress](/apps/wordpress/triggers)
|
- [WordPress](/apps/wordpress/triggers)
|
||||||
|
- [Xero](/apps/xero/triggers)
|
||||||
- [Youtube](/apps/youtube/triggers)
|
- [Youtube](/apps/youtube/triggers)
|
||||||
- [Zendesk](/apps/zendesk/actions)
|
- [Zendesk](/apps/zendesk/actions)
|
||||||
|
1
packages/docs/pages/public/favicons/reddit.svg
Normal file
1
packages/docs/pages/public/favicons/reddit.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="_1O4jTk-dZ-VIxsCuYB6OR8 " width="40" height="48" ><g><circle fill="#FF4500" cx="10" cy="10" r="10"></circle><path fill="#FFFFFF" d="M16.67,10A1.46,1.46,0,0,0,14.2,9a7.12,7.12,0,0,0-3.85-1.23L11,4.65,13.14,5.1a1,1,0,1,0,.13-0.61L10.82,4a0.31,0.31,0,0,0-.37.24L9.71,7.71a7.14,7.14,0,0,0-3.9,1.23A1.46,1.46,0,1,0,4.2,11.33a2.87,2.87,0,0,0,0,.44c0,2.24,2.61,4.06,5.83,4.06s5.83-1.82,5.83-4.06a2.87,2.87,0,0,0,0-.44A1.46,1.46,0,0,0,16.67,10Zm-10,1a1,1,0,1,1,1,1A1,1,0,0,1,6.67,11Zm5.81,2.75a3.84,3.84,0,0,1-2.47.77,3.84,3.84,0,0,1-2.47-.77,0.27,0.27,0,0,1,.38-0.38A3.27,3.27,0,0,0,10,14a3.28,3.28,0,0,0,2.09-.61A0.27,0.27,0,1,1,12.48,13.79Zm-0.18-1.71a1,1,0,1,1,1-1A1,1,0,0,1,12.29,12.08Z"></path></g></svg>
|
After Width: | Height: | Size: 813 B |
@@ -2,12 +2,12 @@ const { AuthenticatedPage } = require('../authenticated-page');
|
|||||||
const { RoleConditionsModal } = require('./role-conditions-modal');
|
const { RoleConditionsModal } = require('./role-conditions-modal');
|
||||||
|
|
||||||
export class AdminCreateRolePage extends AuthenticatedPage {
|
export class AdminCreateRolePage extends AuthenticatedPage {
|
||||||
screenshotPath = '/admin/create-role'
|
screenshotPath = '/admin/create-role';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
constructor (page) {
|
constructor(page) {
|
||||||
super(page);
|
super(page);
|
||||||
this.nameInput = page.getByTestId('name-input');
|
this.nameInput = page.getByTestId('name-input');
|
||||||
this.descriptionInput = page.getByTestId('description-input');
|
this.descriptionInput = page.getByTestId('description-input');
|
||||||
@@ -15,27 +15,28 @@ export class AdminCreateRolePage extends AuthenticatedPage {
|
|||||||
this.connectionRow = page.getByTestId('Connection-permission-row');
|
this.connectionRow = page.getByTestId('Connection-permission-row');
|
||||||
this.executionRow = page.getByTestId('Execution-permission-row');
|
this.executionRow = page.getByTestId('Execution-permission-row');
|
||||||
this.flowRow = page.getByTestId('Flow-permission-row');
|
this.flowRow = page.getByTestId('Flow-permission-row');
|
||||||
|
this.pageTitle = page.getByTestId('create-role-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {('Connection'|'Execution'|'Flow')} subject
|
* @param {('Connection'|'Execution'|'Flow')} subject
|
||||||
*/
|
*/
|
||||||
getRoleConditionsModal (subject) {
|
getRoleConditionsModal(subject) {
|
||||||
return new RoleConditionsModal(this.page, subject);
|
return new RoleConditionsModal(this.page, subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPermissionConfigs () {
|
async getPermissionConfigs() {
|
||||||
const subjects = ['Connection', 'Flow', 'Execution'];
|
const subjects = ['Connection', 'Flow', 'Execution'];
|
||||||
const permissionConfigs = [];
|
const permissionConfigs = [];
|
||||||
for (let subject of subjects) {
|
for (let subject of subjects) {
|
||||||
const row = this.getSubjectRow(subject);
|
const row = this.getSubjectRow(subject);
|
||||||
const actionInputs = await this.getRowInputs(row);
|
const actionInputs = await this.getRowInputs(row);
|
||||||
Object.keys(actionInputs).forEach(action => {
|
Object.keys(actionInputs).forEach((action) => {
|
||||||
permissionConfigs.push({
|
permissionConfigs.push({
|
||||||
action,
|
action,
|
||||||
locator: actionInputs[action],
|
locator: actionInputs[action],
|
||||||
subject,
|
subject,
|
||||||
row
|
row,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -48,35 +49,35 @@ export class AdminCreateRolePage extends AuthenticatedPage {
|
|||||||
* 'Connection' | 'Flow' | 'Execution'
|
* 'Connection' | 'Flow' | 'Execution'
|
||||||
* )} subject
|
* )} subject
|
||||||
*/
|
*/
|
||||||
getSubjectRow (subject) {
|
getSubjectRow(subject) {
|
||||||
const k = `${subject.toLowerCase()}Row`
|
const k = `${subject.toLowerCase()}Row`;
|
||||||
if (this[k]) {
|
if (this[k]) {
|
||||||
return this[k]
|
return this[k];
|
||||||
} else {
|
} else {
|
||||||
throw 'Unknown row'
|
throw 'Unknown row';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Locator} row
|
* @param {import('@playwright/test').Locator} row
|
||||||
*/
|
*/
|
||||||
async getRowInputs (row) {
|
async getRowInputs(row) {
|
||||||
const inputs = {
|
const inputs = {
|
||||||
// settingsButton: row.getByTestId('permission-settings-button')
|
// settingsButton: row.getByTestId('permission-settings-button')
|
||||||
}
|
};
|
||||||
for (let input of ['create', 'read', 'update', 'delete', 'publish']) {
|
for (let input of ['create', 'read', 'update', 'delete', 'publish']) {
|
||||||
const testId = `${input}-checkbox`
|
const testId = `${input}-checkbox`;
|
||||||
if (await row.getByTestId(testId).count() > 0) {
|
if ((await row.getByTestId(testId).count()) > 0) {
|
||||||
inputs[input] = row.getByTestId(testId).locator('input');
|
inputs[input] = row.getByTestId(testId).locator('input');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return inputs
|
return inputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Locator} row
|
* @param {import('@playwright/test').Locator} row
|
||||||
*/
|
*/
|
||||||
async clickPermissionSettings (row) {
|
async clickPermissionSettings(row) {
|
||||||
await row.getByTestId('permission-settings-button').click();
|
await row.getByTestId('permission-settings-button').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ export class AdminCreateRolePage extends AuthenticatedPage {
|
|||||||
* @param {'create'|'read'|'update'|'delete'|'publish'} action
|
* @param {'create'|'read'|'update'|'delete'|'publish'} action
|
||||||
* @param {boolean} val
|
* @param {boolean} val
|
||||||
*/
|
*/
|
||||||
async updateAction (subject, action, val) {
|
async updateAction(subject, action, val) {
|
||||||
const row = await this.getSubjectRow(subject);
|
const row = await this.getSubjectRow(subject);
|
||||||
const inputs = await this.getRowInputs(row);
|
const inputs = await this.getRowInputs(row);
|
||||||
if (inputs[action]) {
|
if (inputs[action]) {
|
||||||
@@ -100,7 +101,7 @@ export class AdminCreateRolePage extends AuthenticatedPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`${subject} does not have action ${action}`)
|
throw new Error(`${subject} does not have action ${action}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -7,24 +7,25 @@ export class AdminCreateUserPage extends AuthenticatedPage {
|
|||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
constructor (page) {
|
constructor(page) {
|
||||||
super(page);
|
super(page);
|
||||||
this.fullNameInput = page.getByTestId('full-name-input');
|
this.fullNameInput = page.getByTestId('full-name-input');
|
||||||
this.emailInput = page.getByTestId('email-input');
|
this.emailInput = page.getByTestId('email-input');
|
||||||
this.passwordInput = page.getByTestId('password-input');
|
this.passwordInput = page.getByTestId('password-input');
|
||||||
this.roleInput = page.getByTestId('role.id-autocomplete');
|
this.roleInput = page.getByTestId('role.id-autocomplete');
|
||||||
this.createButton = page.getByTestId('create-button');
|
this.createButton = page.getByTestId('create-button');
|
||||||
|
this.pageTitle = page.getByTestId('create-user-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
seed (seed) {
|
seed(seed) {
|
||||||
faker.seed(seed || 0);
|
faker.seed(seed || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateUser () {
|
generateUser() {
|
||||||
return {
|
return {
|
||||||
fullName: faker.person.fullName(),
|
fullName: faker.person.fullName(),
|
||||||
email: faker.internet.email().toLowerCase(),
|
email: faker.internet.email().toLowerCase(),
|
||||||
password: faker.internet.password()
|
password: faker.internet.password(),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -5,5 +5,6 @@ export class AdminEditRolePage extends AdminCreateRolePage {
|
|||||||
super(page);
|
super(page);
|
||||||
delete this.createButton;
|
delete this.createButton;
|
||||||
this.updateButton = page.getByTestId('update-button');
|
this.updateButton = page.getByTestId('update-button');
|
||||||
|
this.pageTitle = page.getByTestId('edit-role-title');
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -15,6 +15,7 @@ export class AdminEditUserPage extends AuthenticatedPage {
|
|||||||
this.emailInput = page.getByTestId('email-input');
|
this.emailInput = page.getByTestId('email-input');
|
||||||
this.roleInput = page.getByTestId('role.id-autocomplete');
|
this.roleInput = page.getByTestId('role.id-autocomplete');
|
||||||
this.updateButton = page.getByTestId('update-button');
|
this.updateButton = page.getByTestId('update-button');
|
||||||
|
this.pageTitle = page.getByTestId('edit-user-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
generateUser () {
|
generateUser () {
|
||||||
|
@@ -14,6 +14,7 @@ export class AdminRolesPage extends AuthenticatedPage {
|
|||||||
this.deleteRoleModal = new DeleteRoleModal(page);
|
this.deleteRoleModal = new DeleteRoleModal(page);
|
||||||
this.roleRow = page.getByTestId('role-row');
|
this.roleRow = page.getByTestId('role-row');
|
||||||
this.rolesLoader = page.getByTestId('roles-list-loader');
|
this.rolesLoader = page.getByTestId('roles-list-loader');
|
||||||
|
this.pageTitle = page.getByTestId('roles-page-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,8 +29,9 @@ export class AdminRolesPage extends AuthenticatedPage {
|
|||||||
await this.drawerMenuButton.click();
|
await this.drawerMenuButton.click();
|
||||||
}
|
}
|
||||||
await this.roleDrawerLink.click();
|
await this.roleDrawerLink.click();
|
||||||
|
await this.isMounted();
|
||||||
await this.rolesLoader.waitFor({
|
await this.rolesLoader.waitFor({
|
||||||
state: 'detached',
|
state: 'detached'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
constructor (page) {
|
constructor(page) {
|
||||||
super(page);
|
super(page);
|
||||||
this.createUserButton = page.getByTestId('create-user');
|
this.createUserButton = page.getByTestId('create-user');
|
||||||
this.userRow = page.getByTestId('user-row');
|
this.userRow = page.getByTestId('user-row');
|
||||||
@@ -20,14 +20,16 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
this.nextPageButton = page.getByTestId('next-page-button');
|
this.nextPageButton = page.getByTestId('next-page-button');
|
||||||
this.lastPageButton = page.getByTestId('last-page-button');
|
this.lastPageButton = page.getByTestId('last-page-button');
|
||||||
this.usersLoader = page.getByTestId('users-list-loader');
|
this.usersLoader = page.getByTestId('users-list-loader');
|
||||||
|
this.pageTitle = page.getByTestId('users-page-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigateTo () {
|
async navigateTo() {
|
||||||
await this.profileMenuButton.click();
|
await this.profileMenuButton.click();
|
||||||
await this.adminMenuItem.click();
|
await this.adminMenuItem.click();
|
||||||
|
await this.isMounted();
|
||||||
if (await this.usersLoader.isVisible()) {
|
if (await this.usersLoader.isVisible()) {
|
||||||
await this.usersLoader.waitFor({
|
await this.usersLoader.waitFor({
|
||||||
state: 'detached'
|
state: 'detached',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,36 +37,36 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
/**
|
/**
|
||||||
* @param {string} email
|
* @param {string} email
|
||||||
*/
|
*/
|
||||||
async getUserRowByEmail (email) {
|
async getUserRowByEmail(email) {
|
||||||
return this.userRow.filter({
|
return this.userRow.filter({
|
||||||
has: this.page.getByTestId('user-email').filter({
|
has: this.page.getByTestId('user-email').filter({
|
||||||
hasText: email
|
hasText: email,
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Locator} row
|
* @param {import('@playwright/test').Locator} row
|
||||||
*/
|
*/
|
||||||
async getRowData (row) {
|
async getRowData(row) {
|
||||||
return {
|
return {
|
||||||
fullName: await row.getByTestId('user-full-name').textContent(),
|
fullName: await row.getByTestId('user-full-name').textContent(),
|
||||||
email: await row.getByTestId('user-email').textContent(),
|
email: await row.getByTestId('user-email').textContent(),
|
||||||
role: await row.getByTestId('user-role').textContent()
|
role: await row.getByTestId('user-role').textContent(),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Locator} row
|
* @param {import('@playwright/test').Locator} row
|
||||||
*/
|
*/
|
||||||
async clickEditUser (row) {
|
async clickEditUser(row) {
|
||||||
await row.getByTestId('user-edit').click();
|
await row.getByTestId('user-edit').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Locator} row
|
* @param {import('@playwright/test').Locator} row
|
||||||
*/
|
*/
|
||||||
async clickDeleteUser (row) {
|
async clickDeleteUser(row) {
|
||||||
await row.getByTestId('delete-button').click();
|
await row.getByTestId('delete-button').click();
|
||||||
return this.deleteUserModal;
|
return this.deleteUserModal;
|
||||||
}
|
}
|
||||||
@@ -73,10 +75,10 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
* @param {string} email
|
* @param {string} email
|
||||||
* @returns {import('@playwright/test').Locator | null}
|
* @returns {import('@playwright/test').Locator | null}
|
||||||
*/
|
*/
|
||||||
async findUserPageWithEmail (email) {
|
async findUserPageWithEmail(email) {
|
||||||
if (await this.usersLoader.isVisible()) {
|
if (await this.usersLoader.isVisible()) {
|
||||||
await this.usersLoader.waitFor({
|
await this.usersLoader.waitFor({
|
||||||
state: 'detached'
|
state: 'detached',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// start at the first page
|
// start at the first page
|
||||||
@@ -88,10 +90,11 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
while (true) {
|
while (true) {
|
||||||
if (await this.usersLoader.isVisible()) {
|
if (await this.usersLoader.isVisible()) {
|
||||||
await this.usersLoader.waitFor({
|
await this.usersLoader.waitFor({
|
||||||
state: 'detached'
|
state: 'detached',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const rowLocator = await this.getUserRowByEmail(email);
|
const rowLocator = await this.getUserRowByEmail(email);
|
||||||
|
console.log('rowLocator.count', email, await rowLocator.count());
|
||||||
if ((await rowLocator.count()) === 1) {
|
if ((await rowLocator.count()) === 1) {
|
||||||
return rowLocator;
|
return rowLocator;
|
||||||
}
|
}
|
||||||
@@ -103,7 +106,7 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTotalRows () {
|
async getTotalRows() {
|
||||||
return await this.page.evaluate(() => {
|
return await this.page.evaluate(() => {
|
||||||
const node = document.querySelector('[data-total-count]');
|
const node = document.querySelector('[data-total-count]');
|
||||||
if (node) {
|
if (node) {
|
||||||
@@ -116,7 +119,7 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRowsPerPage () {
|
async getRowsPerPage() {
|
||||||
return await this.page.evaluate(() => {
|
return await this.page.evaluate(() => {
|
||||||
const node = document.querySelector('[data-rows-per-page]');
|
const node = document.querySelector('[data-rows-per-page]');
|
||||||
if (node) {
|
if (node) {
|
||||||
|
@@ -12,7 +12,9 @@ export class AuthenticatedPage extends BasePage {
|
|||||||
|
|
||||||
this.profileMenuButton = this.page.getByTestId('profile-menu-button');
|
this.profileMenuButton = this.page.getByTestId('profile-menu-button');
|
||||||
this.adminMenuItem = this.page.getByRole('menuitem', { name: 'Admin' });
|
this.adminMenuItem = this.page.getByRole('menuitem', { name: 'Admin' });
|
||||||
this.userInterfaceDrawerItem = this.page.getByTestId('user-interface-drawer-link');
|
this.userInterfaceDrawerItem = this.page.getByTestId(
|
||||||
|
'user-interface-drawer-link'
|
||||||
|
);
|
||||||
this.appBar = this.page.getByTestId('app-bar');
|
this.appBar = this.page.getByTestId('app-bar');
|
||||||
this.drawerMenuButton = this.page.getByTestId('drawer-menu-button');
|
this.drawerMenuButton = this.page.getByTestId('drawer-menu-button');
|
||||||
this.goToDashboardButton = this.page.getByTestId('go-back-drawer-link');
|
this.goToDashboardButton = this.page.getByTestId('go-back-drawer-link');
|
||||||
|
@@ -15,6 +15,7 @@ export class BasePage {
|
|||||||
constructor(page) {
|
constructor(page) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.snackbar = page.locator('*[data-test^="snackbar"]');
|
this.snackbar = page.locator('*[data-test^="snackbar"]');
|
||||||
|
this.pageTitle = this.page.getByTestId('page-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,32 +29,32 @@ export class BasePage {
|
|||||||
* }
|
* }
|
||||||
* )}
|
* )}
|
||||||
*/
|
*/
|
||||||
async getSnackbarData (testId) {
|
async getSnackbarData(testId) {
|
||||||
if (!testId) {
|
if (!testId) {
|
||||||
testId = 'snackbar';
|
testId = 'snackbar';
|
||||||
}
|
}
|
||||||
const snack = this.page.getByTestId(testId);
|
const snack = this.page.getByTestId(testId);
|
||||||
return {
|
return {
|
||||||
variant: await snack.getAttribute('data-snackbar-variant'),
|
variant: await snack.getAttribute('data-snackbar-variant'),
|
||||||
text: await snack.evaluate(node => node.innerText),
|
text: await snack.evaluate((node) => node.innerText),
|
||||||
dataset: await snack.evaluate(node => {
|
dataset: await snack.evaluate((node) => {
|
||||||
function getChildren (n) {
|
function getChildren(n) {
|
||||||
return [n].concat(
|
return [n].concat(
|
||||||
...Array.from(n.children).map(c => getChildren(c))
|
...Array.from(n.children).map((c) => getChildren(c))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const datasets = getChildren(node).map(
|
const datasets = getChildren(node).map((n) =>
|
||||||
n => Object.assign({}, n.dataset)
|
Object.assign({}, n.dataset)
|
||||||
);
|
);
|
||||||
return Object.assign({}, ...datasets);
|
return Object.assign({}, ...datasets);
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes all snackbars, should be replaced later
|
* Closes all snackbars, should be replaced later
|
||||||
*/
|
*/
|
||||||
async closeSnackbar () {
|
async closeSnackbar() {
|
||||||
const snackbars = await this.snackbar.all();
|
const snackbars = await this.snackbar.all();
|
||||||
for (const snackbar of snackbars) {
|
for (const snackbar of snackbars) {
|
||||||
await snackbar.click();
|
await snackbar.click();
|
||||||
@@ -78,4 +79,8 @@ export class BasePage {
|
|||||||
|
|
||||||
return await this.page.screenshot({ path: computedPath, ...restOptions });
|
return await this.page.screenshot({ path: computedPath, ...restOptions });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isMounted() {
|
||||||
|
await this.pageTitle.waitFor({ state: 'attached' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,12 +5,12 @@ export class LoginPage extends BasePage {
|
|||||||
static defaultEmail = process.env.LOGIN_EMAIL;
|
static defaultEmail = process.env.LOGIN_EMAIL;
|
||||||
static defaultPassword = process.env.LOGIN_PASSWORD;
|
static defaultPassword = process.env.LOGIN_PASSWORD;
|
||||||
|
|
||||||
static setDefaultLogin (email, password) {
|
static setDefaultLogin(email, password) {
|
||||||
this.defaultEmail = email;
|
this.defaultEmail = email;
|
||||||
this.defaultPassword = password;
|
this.defaultPassword = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
static resetDefaultLogin () {
|
static resetDefaultLogin() {
|
||||||
this.defaultEmail = process.env.LOGIN_EMAIL;
|
this.defaultEmail = process.env.LOGIN_EMAIL;
|
||||||
this.defaultPassword = process.env.LOGIN_PASSWORD;
|
this.defaultPassword = process.env.LOGIN_PASSWORD;
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@ export class LoginPage extends BasePage {
|
|||||||
this.emailTextField = this.page.getByTestId('email-text-field');
|
this.emailTextField = this.page.getByTestId('email-text-field');
|
||||||
this.passwordTextField = this.page.getByTestId('password-text-field');
|
this.passwordTextField = this.page.getByTestId('password-text-field');
|
||||||
this.loginButton = this.page.getByTestId('login-button');
|
this.loginButton = this.page.getByTestId('login-button');
|
||||||
|
this.pageTitle = this.page.getByTestId('login-form-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
async open() {
|
async open() {
|
||||||
|
@@ -22,6 +22,7 @@ test.describe('Role management page', () => {
|
|||||||
await test.step('Create a new role', async () => {
|
await test.step('Create a new role', async () => {
|
||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
|
await adminCreateRolePage.isMounted();
|
||||||
await adminCreateRolePage.nameInput.fill('Create Edit Test');
|
await adminCreateRolePage.nameInput.fill('Create Edit Test');
|
||||||
await adminCreateRolePage.descriptionInput.fill('Test description');
|
await adminCreateRolePage.descriptionInput.fill('Test description');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
@@ -54,6 +55,7 @@ test.describe('Role management page', () => {
|
|||||||
|
|
||||||
await test.step('Edit the role', async () => {
|
await test.step('Edit the role', async () => {
|
||||||
await adminRolesPage.clickEditRole(roleRow);
|
await adminRolesPage.clickEditRole(roleRow);
|
||||||
|
await adminEditRolePage.isMounted();
|
||||||
await adminEditRolePage.nameInput.fill('Create Update Test');
|
await adminEditRolePage.nameInput.fill('Create Update Test');
|
||||||
await adminEditRolePage.descriptionInput.fill('Update test description');
|
await adminEditRolePage.descriptionInput.fill('Update test description');
|
||||||
await adminEditRolePage.updateButton.click();
|
await adminEditRolePage.updateButton.click();
|
||||||
@@ -70,6 +72,7 @@ test.describe('Role management page', () => {
|
|||||||
roleRow = await test.step(
|
roleRow = await test.step(
|
||||||
'Make sure changes reflected on roles page',
|
'Make sure changes reflected on roles page',
|
||||||
async () => {
|
async () => {
|
||||||
|
await adminRolesPage.isMounted();
|
||||||
const roleRow = await adminRolesPage.getRoleRowByName(
|
const roleRow = await adminRolesPage.getRoleRowByName(
|
||||||
'Create Update Test'
|
'Create Update Test'
|
||||||
);
|
);
|
||||||
@@ -110,6 +113,8 @@ test.describe('Role management page', () => {
|
|||||||
// This test breaks right now
|
// This test breaks right now
|
||||||
test.skip('Make sure create/edit role page is scrollable', async ({
|
test.skip('Make sure create/edit role page is scrollable', async ({
|
||||||
adminRolesPage,
|
adminRolesPage,
|
||||||
|
adminEditRolePage,
|
||||||
|
adminCreateRolePage,
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const initViewportSize = page.viewportSize;
|
const initViewportSize = page.viewportSize;
|
||||||
@@ -121,6 +126,7 @@ test.describe('Role management page', () => {
|
|||||||
await test.step('Ensure create role page is scrollable', async () => {
|
await test.step('Ensure create role page is scrollable', async () => {
|
||||||
await adminRolesPage.navigateTo(true);
|
await adminRolesPage.navigateTo(true);
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
|
await adminCreateRolePage.isMounted();
|
||||||
|
|
||||||
const initScrollTop = await page.evaluate(() => {
|
const initScrollTop = await page.evaluate(() => {
|
||||||
return document.documentElement.scrollTop;
|
return document.documentElement.scrollTop;
|
||||||
@@ -138,6 +144,7 @@ test.describe('Role management page', () => {
|
|||||||
await adminRolesPage.navigateTo(true);
|
await adminRolesPage.navigateTo(true);
|
||||||
const adminRow = await adminRolesPage.getRoleRowByName('Admin');
|
const adminRow = await adminRolesPage.getRoleRowByName('Admin');
|
||||||
await adminRolesPage.clickEditRole(adminRow);
|
await adminRolesPage.clickEditRole(adminRow);
|
||||||
|
await adminEditRolePage.isMounted();
|
||||||
|
|
||||||
const initScrollTop = await page.evaluate(() => {
|
const initScrollTop = await page.evaluate(() => {
|
||||||
return document.documentElement.scrollTop;
|
return document.documentElement.scrollTop;
|
||||||
@@ -166,6 +173,7 @@ test.describe('Role management page', () => {
|
|||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
await test.step('Create a new role', async () => {
|
await test.step('Create a new role', async () => {
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
|
await adminCreateRolePage.isMounted();
|
||||||
await adminCreateRolePage.nameInput.fill('Delete Role');
|
await adminCreateRolePage.nameInput.fill('Delete Role');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
await adminCreateRolePage.snackbar.waitFor({
|
await adminCreateRolePage.snackbar.waitFor({
|
||||||
@@ -268,6 +276,7 @@ test.describe('Role management page', () => {
|
|||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
await test.step('Create a new role', async () => {
|
await test.step('Create a new role', async () => {
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
|
await adminCreateRolePage.isMounted();
|
||||||
await adminCreateRolePage.nameInput.fill('Cannot Delete Role');
|
await adminCreateRolePage.nameInput.fill('Cannot Delete Role');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
await adminCreateRolePage.snackbar.waitFor({
|
await adminCreateRolePage.snackbar.waitFor({
|
||||||
@@ -282,6 +291,7 @@ test.describe('Role management page', () => {
|
|||||||
await test.step('Create a new user with this role', async () => {
|
await test.step('Create a new user with this role', async () => {
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
|
await adminCreateUserPage.isMounted();
|
||||||
await adminCreateUserPage.fullNameInput.fill('User Delete Role Test');
|
await adminCreateUserPage.fullNameInput.fill('User Delete Role Test');
|
||||||
await adminCreateUserPage.emailInput.fill(
|
await adminCreateUserPage.emailInput.fill(
|
||||||
'user-delete-role-test@automatisch.io'
|
'user-delete-role-test@automatisch.io'
|
||||||
@@ -306,6 +316,7 @@ test.describe('Role management page', () => {
|
|||||||
const row = await adminUsersPage.findUserPageWithEmail(
|
const row = await adminUsersPage.findUserPageWithEmail(
|
||||||
'user-delete-role-test@automatisch.io'
|
'user-delete-role-test@automatisch.io'
|
||||||
);
|
);
|
||||||
|
// await test.waitForTimeout(10000);
|
||||||
const modal = await adminUsersPage.clickDeleteUser(row);
|
const modal = await adminUsersPage.clickDeleteUser(row);
|
||||||
await modal.deleteButton.click();
|
await modal.deleteButton.click();
|
||||||
await adminUsersPage.snackbar.waitFor({
|
await adminUsersPage.snackbar.waitFor({
|
||||||
@@ -348,6 +359,7 @@ test('Accessibility of role management page', async ({
|
|||||||
await test.step('Create the basic test role', async () => {
|
await test.step('Create the basic test role', async () => {
|
||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
|
await adminCreateRolePage.isMounted();
|
||||||
await adminCreateRolePage.nameInput.fill('Basic Test');
|
await adminCreateRolePage.nameInput.fill('Basic Test');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
await adminCreateRolePage.snackbar.waitFor({
|
await adminCreateRolePage.snackbar.waitFor({
|
||||||
@@ -363,6 +375,7 @@ test('Accessibility of role management page', async ({
|
|||||||
await test.step('Create a new user with the basic role', async () => {
|
await test.step('Create a new user with the basic role', async () => {
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
|
await adminCreateUserPage.isMounted();
|
||||||
await adminCreateUserPage.fullNameInput.fill('Role Test');
|
await adminCreateUserPage.fullNameInput.fill('Role Test');
|
||||||
await adminCreateUserPage.emailInput.fill('basic-role-test@automatisch.io');
|
await adminCreateUserPage.emailInput.fill('basic-role-test@automatisch.io');
|
||||||
await adminCreateUserPage.passwordInput.fill('sample');
|
await adminCreateUserPage.passwordInput.fill('sample');
|
||||||
@@ -378,7 +391,7 @@ test('Accessibility of role management page', async ({
|
|||||||
'snackbar-create-user-success'
|
'snackbar-create-user-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminCreateRolePage.closeSnackbar();
|
await adminCreateUserPage.closeSnackbar();
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Logout and login to the basic role user', async () => {
|
await test.step('Logout and login to the basic role user', async () => {
|
||||||
@@ -386,6 +399,7 @@ test('Accessibility of role management page', async ({
|
|||||||
await page.getByTestId('logout-item').click();
|
await page.getByTestId('logout-item').click();
|
||||||
// await page.reload({ waitUntil: 'networkidle' });
|
// await page.reload({ waitUntil: 'networkidle' });
|
||||||
const loginPage = new LoginPage(page);
|
const loginPage = new LoginPage(page);
|
||||||
|
// await loginPage.isMounted();
|
||||||
await loginPage.login('basic-role-test@automatisch.io', 'sample');
|
await loginPage.login('basic-role-test@automatisch.io', 'sample');
|
||||||
await expect(loginPage.loginButton).not.toBeVisible();
|
await expect(loginPage.loginButton).not.toBeVisible();
|
||||||
await expect(page).toHaveURL('/flows');
|
await expect(page).toHaveURL('/flows');
|
||||||
@@ -414,6 +428,7 @@ test('Accessibility of role management page', async ({
|
|||||||
await page.getByTestId('profile-menu-button').click();
|
await page.getByTestId('profile-menu-button').click();
|
||||||
await page.getByTestId('logout-item').click();
|
await page.getByTestId('logout-item').click();
|
||||||
const loginPage = new LoginPage(page);
|
const loginPage = new LoginPage(page);
|
||||||
|
await loginPage.isMounted();
|
||||||
await loginPage.login();
|
await loginPage.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -423,6 +438,7 @@ test('Accessibility of role management page', async ({
|
|||||||
'basic-role-test@automatisch.io'
|
'basic-role-test@automatisch.io'
|
||||||
);
|
);
|
||||||
await adminUsersPage.clickEditUser(row);
|
await adminUsersPage.clickEditUser(row);
|
||||||
|
await adminEditUserPage.isMounted();
|
||||||
await adminEditUserPage.roleInput.click();
|
await adminEditUserPage.roleInput.click();
|
||||||
await adminEditUserPage.page.getByRole('option', { name: 'Admin' }).click();
|
await adminEditUserPage.page.getByRole('option', { name: 'Admin' }).click();
|
||||||
await adminEditUserPage.updateButton.click();
|
await adminEditUserPage.updateButton.click();
|
||||||
|
1
packages/types/index.d.ts
vendored
1
packages/types/index.d.ts
vendored
@@ -462,6 +462,7 @@ type AppAuthClient = {
|
|||||||
appConfigId: string;
|
appConfigId: string;
|
||||||
authDefaults: string;
|
authDefaults: string;
|
||||||
formattedAuthDefaults: IJSONObject;
|
formattedAuthDefaults: IJSONObject;
|
||||||
|
active: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Notification = {
|
type Notification = {
|
||||||
|
@@ -0,0 +1,104 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { IField } from '@automatisch/types';
|
||||||
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||||
|
import type { UseFormProps } from 'react-hook-form';
|
||||||
|
import type { ApolloError } from '@apollo/client';
|
||||||
|
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import InputCreator from 'components/InputCreator';
|
||||||
|
import Switch from 'components/Switch';
|
||||||
|
import TextField from 'components/TextField';
|
||||||
|
|
||||||
|
import { Form } from './style';
|
||||||
|
|
||||||
|
type AdminApplicationAuthClientDialogProps = {
|
||||||
|
title: string;
|
||||||
|
authFields?: IField[];
|
||||||
|
defaultValues: UseFormProps['defaultValues'];
|
||||||
|
loading: boolean;
|
||||||
|
submitting: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
error?: ApolloError;
|
||||||
|
submitHandler: SubmitHandler<FieldValues>;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdminApplicationAuthClientDialog(
|
||||||
|
props: AdminApplicationAuthClientDialogProps
|
||||||
|
): React.ReactElement {
|
||||||
|
const {
|
||||||
|
error,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
loading,
|
||||||
|
submitHandler,
|
||||||
|
authFields,
|
||||||
|
submitting,
|
||||||
|
defaultValues,
|
||||||
|
disabled = false,
|
||||||
|
} = props;
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onClose={onClose}>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
{error && (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
||||||
|
>
|
||||||
|
{error.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<DialogContent>
|
||||||
|
{loading ? (
|
||||||
|
<CircularProgress
|
||||||
|
data-test="search-for-app-loader"
|
||||||
|
sx={{ display: 'block', margin: '20px auto' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DialogContentText tabIndex={-1} component="div">
|
||||||
|
<Form
|
||||||
|
onSubmit={submitHandler}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
render={({ formState: { isDirty } }) => (
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
name="active"
|
||||||
|
label={formatMessage('authClient.inputActive')}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
required={true}
|
||||||
|
name="name"
|
||||||
|
label={formatMessage('authClient.inputName')}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
{authFields?.map((field: IField) => (
|
||||||
|
<InputCreator key={field.key} schema={field} />
|
||||||
|
))}
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ boxShadow: 2 }}
|
||||||
|
loading={submitting}
|
||||||
|
disabled={disabled || !isDirty}
|
||||||
|
>
|
||||||
|
{formatMessage('authClient.buttonSubmit')}
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
></Form>
|
||||||
|
</DialogContentText>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import BaseForm from 'components/Form';
|
||||||
|
|
||||||
|
export const Form = styled(BaseForm)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
paddingTop: theme.spacing(1),
|
||||||
|
}));
|
@@ -0,0 +1,89 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardActionArea from '@mui/material/CardActionArea';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
|
import * as URLS from 'config/urls';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
||||||
|
|
||||||
|
import NoResultFound from 'components/NoResultFound';
|
||||||
|
|
||||||
|
type AdminApplicationAuthClientsProps = {
|
||||||
|
appKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function AdminApplicationAuthClients(
|
||||||
|
props: AdminApplicationAuthClientsProps
|
||||||
|
): React.ReactElement {
|
||||||
|
const { appKey } = props;
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const { appAuthClients, loading } = useAppAuthClients({ appKey });
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
|
||||||
|
|
||||||
|
if (!appAuthClients?.length) {
|
||||||
|
return (
|
||||||
|
<NoResultFound
|
||||||
|
to={URLS.ADMIN_APP_AUTH_CLIENTS_CREATE(appKey)}
|
||||||
|
text={formatMessage('adminAppsAuthClients.noAuthClients')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedAuthClients = appAuthClients.slice().sort((a, b) => {
|
||||||
|
if (a.id < b.id) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.id > b.id) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{sortedAuthClients.map((client) => (
|
||||||
|
<Card sx={{ mb: 1 }} key={client.id}>
|
||||||
|
<CardActionArea
|
||||||
|
component={Link}
|
||||||
|
to={URLS.ADMIN_APP_AUTH_CLIENT(appKey, client.id)}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<Typography variant="h6" noWrap>
|
||||||
|
{client.name}
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
color={client?.active ? 'success' : 'info'}
|
||||||
|
variant={client?.active ? 'filled' : 'outlined'}
|
||||||
|
label={formatMessage(
|
||||||
|
client?.active
|
||||||
|
? 'adminAppsAuthClients.statusActive'
|
||||||
|
: 'adminAppsAuthClients.statusInactive'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<Stack justifyContent="flex-end" direction="row">
|
||||||
|
<Link to={URLS.ADMIN_APP_AUTH_CLIENTS_CREATE(appKey)}>
|
||||||
|
<Button variant="contained" sx={{ mt: 2 }} component="div">
|
||||||
|
{formatMessage('createAuthClient.button')}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminApplicationAuthClients;
|
@@ -0,0 +1,112 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import type { IApp } from '@automatisch/types';
|
||||||
|
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||||
|
import { useMutation } from '@apollo/client';
|
||||||
|
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
||||||
|
import { CREATE_APP_AUTH_CLIENT } from 'graphql/mutations/create-app-auth-client';
|
||||||
|
|
||||||
|
import useAppConfig from 'hooks/useAppConfig.ee';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
|
import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
||||||
|
|
||||||
|
type AdminApplicationCreateAuthClientProps = {
|
||||||
|
appKey: string;
|
||||||
|
application: IApp;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdminApplicationCreateAuthClient(
|
||||||
|
props: AdminApplicationCreateAuthClientProps
|
||||||
|
): React.ReactElement {
|
||||||
|
const { appKey, application, onClose } = props;
|
||||||
|
const { auth } = application;
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const { appConfig, loading: loadingAppConfig } = useAppConfig(appKey);
|
||||||
|
const [
|
||||||
|
createAppConfig,
|
||||||
|
{ loading: loadingCreateAppConfig, error: createAppConfigError },
|
||||||
|
] = useMutation(CREATE_APP_CONFIG, {
|
||||||
|
refetchQueries: ['GetAppConfig'],
|
||||||
|
context: { autoSnackbar: false },
|
||||||
|
});
|
||||||
|
const [
|
||||||
|
createAppAuthClient,
|
||||||
|
{ loading: loadingCreateAppAuthClient, error: createAppAuthClientError },
|
||||||
|
] = useMutation(CREATE_APP_AUTH_CLIENT, {
|
||||||
|
refetchQueries: ['GetAppAuthClients'],
|
||||||
|
context: { autoSnackbar: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitHandler: SubmitHandler<FieldValues> = async (values) => {
|
||||||
|
let appConfigId = appConfig?.id;
|
||||||
|
|
||||||
|
if (!appConfigId) {
|
||||||
|
const { data: appConfigData } = await createAppConfig({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
key: appKey,
|
||||||
|
allowCustomConnection: false,
|
||||||
|
shared: false,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
appConfigId = appConfigData.createAppConfig.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, active, ...formattedAuthDefaults } = values;
|
||||||
|
|
||||||
|
await createAppAuthClient({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
appConfigId,
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
formattedAuthDefaults,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAuthFieldsDefaultValues = useCallback(() => {
|
||||||
|
if (!auth?.fields) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const defaultValues: {
|
||||||
|
[key: string]: any;
|
||||||
|
} = {};
|
||||||
|
auth.fields.forEach((field) => {
|
||||||
|
if (field.value || field.type !== 'string') {
|
||||||
|
defaultValues[field.key] = field.value;
|
||||||
|
} else if (field.type === 'string') {
|
||||||
|
defaultValues[field.key] = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return defaultValues;
|
||||||
|
}, [auth?.fields]);
|
||||||
|
|
||||||
|
const defaultValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
name: '',
|
||||||
|
active: false,
|
||||||
|
...getAuthFieldsDefaultValues(),
|
||||||
|
}),
|
||||||
|
[getAuthFieldsDefaultValues]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminApplicationAuthClientDialog
|
||||||
|
onClose={onClose}
|
||||||
|
error={createAppConfigError || createAppAuthClientError}
|
||||||
|
title={formatMessage('createAuthClient.title')}
|
||||||
|
loading={loadingAppConfig}
|
||||||
|
submitHandler={submitHandler}
|
||||||
|
authFields={auth?.fields}
|
||||||
|
submitting={loadingCreateAppConfig || loadingCreateAppAuthClient}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,95 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import type { IApp } from '@automatisch/types';
|
||||||
|
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||||
|
import { useMutation } from '@apollo/client';
|
||||||
|
import { UPDATE_APP_AUTH_CLIENT } from 'graphql/mutations/update-app-auth-client';
|
||||||
|
|
||||||
|
import useAppAuthClient from 'hooks/useAppAuthClient.ee';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
||||||
|
|
||||||
|
type AdminApplicationUpdateAuthClientProps = {
|
||||||
|
application: IApp;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdminApplicationUpdateAuthClient(
|
||||||
|
props: AdminApplicationUpdateAuthClientProps
|
||||||
|
): React.ReactElement {
|
||||||
|
const { application, onClose } = props;
|
||||||
|
const { auth } = application;
|
||||||
|
const authFields = auth?.fields?.map((field) => ({
|
||||||
|
...field,
|
||||||
|
required: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
|
const { clientId } = useParams();
|
||||||
|
const { appAuthClient, loading: loadingAuthClient } =
|
||||||
|
useAppAuthClient(clientId);
|
||||||
|
const [updateAppAuthClient, { loading: loadingUpdateAppAuthClient, error }] =
|
||||||
|
useMutation(UPDATE_APP_AUTH_CLIENT, {
|
||||||
|
refetchQueries: ['GetAppAuthClients'],
|
||||||
|
context: { autoSnackbar: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitHandler: SubmitHandler<FieldValues> = async (values) => {
|
||||||
|
if (!appAuthClient) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { name, active, ...formattedAuthDefaults } = values;
|
||||||
|
await updateAppAuthClient({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: appAuthClient.id,
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
formattedAuthDefaults,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAuthFieldsDefaultValues = useCallback(() => {
|
||||||
|
if (!authFields) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const defaultValues: {
|
||||||
|
[key: string]: any;
|
||||||
|
} = {};
|
||||||
|
authFields.forEach((field) => {
|
||||||
|
if (field.value || field.type !== 'string') {
|
||||||
|
defaultValues[field.key] = field.value;
|
||||||
|
} else if (field.type === 'string') {
|
||||||
|
defaultValues[field.key] = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return defaultValues;
|
||||||
|
}, [auth?.fields]);
|
||||||
|
|
||||||
|
const defaultValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
name: appAuthClient?.name || '',
|
||||||
|
active: appAuthClient?.active || false,
|
||||||
|
...getAuthFieldsDefaultValues(),
|
||||||
|
}),
|
||||||
|
[appAuthClient, getAuthFieldsDefaultValues]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminApplicationAuthClientDialog
|
||||||
|
onClose={onClose}
|
||||||
|
error={error}
|
||||||
|
title={formatMessage('updateAuthClient.title')}
|
||||||
|
loading={loadingAuthClient}
|
||||||
|
submitHandler={submitHandler}
|
||||||
|
authFields={authFields}
|
||||||
|
submitting={loadingUpdateAppAuthClient}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
disabled={!appAuthClient}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -17,7 +17,7 @@ type AppAuthClientsDialogProps = {
|
|||||||
|
|
||||||
export default function AppAuthClientsDialog(props: AppAuthClientsDialogProps) {
|
export default function AppAuthClientsDialog(props: AppAuthClientsDialogProps) {
|
||||||
const { appKey, onClientClick, onClose } = props;
|
const { appKey, onClientClick, onClose } = props;
|
||||||
const { appAuthClients } = useAppAuthClients(appKey);
|
const { appAuthClients } = useAppAuthClients({ appKey, active: true });
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
|
@@ -11,15 +11,21 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
|||||||
type ListLoaderProps = {
|
type ListLoaderProps = {
|
||||||
rowsNumber: number;
|
rowsNumber: number;
|
||||||
columnsNumber: number;
|
columnsNumber: number;
|
||||||
|
'data-test'?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListLoader = ({ rowsNumber, columnsNumber }: ListLoaderProps) => {
|
const ListLoader = ({
|
||||||
|
rowsNumber,
|
||||||
|
columnsNumber,
|
||||||
|
'data-test': dataTest,
|
||||||
|
}: ListLoaderProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{[...Array(rowsNumber)].map((row, index) => (
|
{[...Array(rowsNumber)].map((row, index) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={index}
|
key={index}
|
||||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||||
|
data-test={dataTest && index === 0 ? dataTest : undefined}
|
||||||
>
|
>
|
||||||
{[...Array(columnsNumber)].map((cell, index) => (
|
{[...Array(columnsNumber)].map((cell, index) => (
|
||||||
<TableCell key={index} scope="row">
|
<TableCell key={index} scope="row">
|
||||||
|
@@ -44,6 +44,7 @@ function LoginForm() {
|
|||||||
<Typography
|
<Typography
|
||||||
variant="h3"
|
variant="h3"
|
||||||
align="center"
|
align="center"
|
||||||
|
data-test="login-form-title"
|
||||||
sx={{
|
sx={{
|
||||||
borderBottom: '1px solid',
|
borderBottom: '1px solid',
|
||||||
borderColor: (theme) => theme.palette.text.disabled,
|
borderColor: (theme) => theme.palette.text.disabled,
|
||||||
@@ -78,13 +79,15 @@ function LoginForm() {
|
|||||||
sx={{ mb: 1 }}
|
sx={{ mb: 1 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isCloud && <Link
|
{isCloud && (
|
||||||
|
<Link
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to={URLS.FORGOT_PASSWORD}
|
to={URLS.FORGOT_PASSWORD}
|
||||||
underline="none"
|
underline="none"
|
||||||
>
|
>
|
||||||
{formatMessage('loginForm.forgotPasswordText')}
|
{formatMessage('loginForm.forgotPasswordText')}
|
||||||
</Link>}
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -98,13 +101,15 @@ function LoginForm() {
|
|||||||
{formatMessage('loginForm.submit')}
|
{formatMessage('loginForm.submit')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
|
||||||
{isCloud && <Typography variant="body1" align="center" mt={3}>
|
{isCloud && (
|
||||||
|
<Typography variant="body1" align="center" mt={3}>
|
||||||
{formatMessage('loginForm.noAccount')}
|
{formatMessage('loginForm.noAccount')}
|
||||||
|
|
||||||
<Link component={RouterLink} to={URLS.SIGNUP} underline="none">
|
<Link component={RouterLink} to={URLS.SIGNUP} underline="none">
|
||||||
{formatMessage('loginForm.signUp')}
|
{formatMessage('loginForm.signUp')}
|
||||||
</Link>
|
</Link>
|
||||||
</Typography>}
|
</Typography>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@@ -4,5 +4,5 @@ import Typography, { TypographyProps } from '@mui/material/Typography';
|
|||||||
type PageTitleProps = TypographyProps;
|
type PageTitleProps = TypographyProps;
|
||||||
|
|
||||||
export default function PageTitle(props: PageTitleProps): React.ReactElement {
|
export default function PageTitle(props: PageTitleProps): React.ReactElement {
|
||||||
return <Typography variant="h3" {...props} />;
|
return <Typography variant="h3" data-test="page-title" {...props} />;
|
||||||
}
|
}
|
||||||
|
@@ -107,6 +107,10 @@ export const ADMIN_APP_SETTINGS = (appKey: string) =>
|
|||||||
`${ADMIN_SETTINGS}/apps/${appKey}/settings`;
|
`${ADMIN_SETTINGS}/apps/${appKey}/settings`;
|
||||||
export const ADMIN_APP_AUTH_CLIENTS = (appKey: string) =>
|
export const ADMIN_APP_AUTH_CLIENTS = (appKey: string) =>
|
||||||
`${ADMIN_SETTINGS}/apps/${appKey}/auth-clients`;
|
`${ADMIN_SETTINGS}/apps/${appKey}/auth-clients`;
|
||||||
|
export const ADMIN_APP_AUTH_CLIENT = (appKey: string, id: string) =>
|
||||||
|
`${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/${id}`;
|
||||||
|
export const ADMIN_APP_AUTH_CLIENTS_CREATE = (appKey: string) =>
|
||||||
|
`${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/create`;
|
||||||
|
|
||||||
export const DASHBOARD = FLOWS;
|
export const DASHBOARD = FLOWS;
|
||||||
|
|
||||||
|
12
packages/web/src/graphql/mutations/create-app-auth-client.ts
Normal file
12
packages/web/src/graphql/mutations/create-app-auth-client.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const CREATE_APP_AUTH_CLIENT = gql`
|
||||||
|
mutation CreateAppAuthClient($input: CreateAppAuthClientInput) {
|
||||||
|
createAppAuthClient(input: $input) {
|
||||||
|
id
|
||||||
|
appConfigId
|
||||||
|
name
|
||||||
|
active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
12
packages/web/src/graphql/mutations/update-app-auth-client.ts
Normal file
12
packages/web/src/graphql/mutations/update-app-auth-client.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const UPDATE_APP_AUTH_CLIENT = gql`
|
||||||
|
mutation UpdateAppAuthClient($input: UpdateAppAuthClientInput) {
|
||||||
|
updateAppAuthClient(input: $input) {
|
||||||
|
id
|
||||||
|
appConfigId
|
||||||
|
name
|
||||||
|
active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@@ -1,28 +1,26 @@
|
|||||||
import { useLazyQuery } from '@apollo/client';
|
import { useLazyQuery } from '@apollo/client';
|
||||||
import { AppConfig } from '@automatisch/types';
|
import { AppAuthClient } from '@automatisch/types';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { GET_APP_AUTH_CLIENT } from 'graphql/queries/get-app-auth-client.ee';
|
import { GET_APP_AUTH_CLIENT } from 'graphql/queries/get-app-auth-client.ee';
|
||||||
|
|
||||||
type QueryResponse = {
|
type QueryResponse = {
|
||||||
getAppAuthClient: AppConfig;
|
getAppAuthClient: AppAuthClient;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function useAppAuthClient(id: string) {
|
export default function useAppAuthClient(id?: string) {
|
||||||
const [
|
const [getAppAuthClient, { data, loading }] =
|
||||||
getAppAuthClient,
|
useLazyQuery<QueryResponse>(GET_APP_AUTH_CLIENT);
|
||||||
{
|
|
||||||
data,
|
|
||||||
loading
|
|
||||||
}
|
|
||||||
] = useLazyQuery<QueryResponse>(GET_APP_AUTH_CLIENT);
|
|
||||||
const appAuthClient = data?.getAppAuthClient;
|
const appAuthClient = data?.getAppAuthClient;
|
||||||
|
|
||||||
React.useEffect(function fetchUponId() {
|
React.useEffect(
|
||||||
|
function fetchUponId() {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
getAppAuthClient({ variables: { id } });
|
getAppAuthClient({ variables: { id } });
|
||||||
}, [id]);
|
},
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appAuthClient,
|
appAuthClient,
|
||||||
|
@@ -6,25 +6,33 @@ import { GET_APP_AUTH_CLIENTS } from 'graphql/queries/get-app-auth-clients.ee';
|
|||||||
|
|
||||||
type QueryResponse = {
|
type QueryResponse = {
|
||||||
getAppAuthClients: AppAuthClient[];
|
getAppAuthClients: AppAuthClient[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function useAppAuthClient(appKey: string) {
|
export default function useAppAuthClient({
|
||||||
const [
|
appKey,
|
||||||
getAppAuthClients,
|
active,
|
||||||
|
}: {
|
||||||
|
appKey: string;
|
||||||
|
active?: boolean;
|
||||||
|
}) {
|
||||||
|
const [getAppAuthClients, { data, loading }] = useLazyQuery<QueryResponse>(
|
||||||
|
GET_APP_AUTH_CLIENTS,
|
||||||
{
|
{
|
||||||
data,
|
|
||||||
loading
|
|
||||||
}
|
|
||||||
] = useLazyQuery<QueryResponse>(GET_APP_AUTH_CLIENTS, {
|
|
||||||
context: { autoSnackbar: false },
|
context: { autoSnackbar: false },
|
||||||
});
|
}
|
||||||
|
);
|
||||||
const appAuthClients = data?.getAppAuthClients;
|
const appAuthClients = data?.getAppAuthClients;
|
||||||
|
|
||||||
React.useEffect(function fetchUponAppKey() {
|
React.useEffect(
|
||||||
|
function fetchUponAppKey() {
|
||||||
if (!appKey) return;
|
if (!appKey) return;
|
||||||
|
|
||||||
getAppAuthClients({ variables: { appKey, active: true } });
|
getAppAuthClients({
|
||||||
}, [appKey]);
|
variables: { appKey, ...(typeof active === 'boolean' && { active }) },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[appKey]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appAuthClients,
|
appAuthClients,
|
||||||
|
@@ -256,5 +256,14 @@
|
|||||||
"adminAppsSettings.shared": "Shared",
|
"adminAppsSettings.shared": "Shared",
|
||||||
"adminAppsSettings.disabled": "Disabled",
|
"adminAppsSettings.disabled": "Disabled",
|
||||||
"adminAppsSettings.save": "Save",
|
"adminAppsSettings.save": "Save",
|
||||||
"adminAppsSettings.successfullySaved": "Settings have been saved."
|
"adminAppsSettings.successfullySaved": "Settings have been saved.",
|
||||||
|
"adminAppsAuthClients.noAuthClients": "You don't have any auth clients yet.",
|
||||||
|
"adminAppsAuthClients.statusActive": "Active",
|
||||||
|
"adminAppsAuthClients.statusInactive": "Inactive",
|
||||||
|
"createAuthClient.button": "Create auth client",
|
||||||
|
"createAuthClient.title": "Create auth client",
|
||||||
|
"authClient.buttonSubmit": "Submit",
|
||||||
|
"authClient.inputName": "Name",
|
||||||
|
"authClient.inputActive": "Active",
|
||||||
|
"updateAuthClient.title": "Update auth client"
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import {
|
|||||||
Routes,
|
Routes,
|
||||||
useParams,
|
useParams,
|
||||||
useMatch,
|
useMatch,
|
||||||
|
useNavigate,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
@@ -23,6 +24,9 @@ import AppIcon from 'components/AppIcon';
|
|||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
import AdminApplicationSettings from 'components/AdminApplicationSettings';
|
import AdminApplicationSettings from 'components/AdminApplicationSettings';
|
||||||
|
import AdminApplicationAuthClients from 'components/AdminApplicationAuthClients';
|
||||||
|
import AdminApplicationCreateAuthClient from 'components/AdminApplicationCreateAuthClient';
|
||||||
|
import AdminApplicationUpdateAuthClient from 'components/AdminApplicationUpdateAuthClient';
|
||||||
|
|
||||||
type AdminApplicationParams = {
|
type AdminApplicationParams = {
|
||||||
appKey: string;
|
appKey: string;
|
||||||
@@ -32,6 +36,7 @@ export default function AdminApplication(): React.ReactElement | null {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const connectionsPathMatch = useMatch({
|
const connectionsPathMatch = useMatch({
|
||||||
path: URLS.ADMIN_APP_CONNECTIONS_PATTERN,
|
path: URLS.ADMIN_APP_CONNECTIONS_PATTERN,
|
||||||
@@ -51,9 +56,12 @@ export default function AdminApplication(): React.ReactElement | null {
|
|||||||
|
|
||||||
const app = data?.getApp || {};
|
const app = data?.getApp || {};
|
||||||
|
|
||||||
|
const goToAuthClientsPage = () => navigate('auth-clients');
|
||||||
|
|
||||||
if (loading) return null;
|
if (loading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
<Grid container item xs={12} sm={10} md={9}>
|
<Grid container item xs={12} sm={10} md={9}>
|
||||||
<Grid container sx={{ mb: 3 }} alignItems="center">
|
<Grid container sx={{ mb: 3 }} alignItems="center">
|
||||||
@@ -108,7 +116,7 @@ export default function AdminApplication(): React.ReactElement | null {
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={`/auth-clients/*`}
|
path={`/auth-clients/*`}
|
||||||
element={<div>Auth clients</div>}
|
element={<AdminApplicationAuthClients appKey={appKey} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={`/connections/*`}
|
path={`/connections/*`}
|
||||||
@@ -125,5 +133,27 @@ export default function AdminApplication(): React.ReactElement | null {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/auth-clients/create"
|
||||||
|
element={
|
||||||
|
<AdminApplicationCreateAuthClient
|
||||||
|
application={app}
|
||||||
|
onClose={goToAuthClientsPage}
|
||||||
|
appKey={appKey}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/auth-clients/:clientId"
|
||||||
|
element={
|
||||||
|
<AdminApplicationUpdateAuthClient
|
||||||
|
application={app}
|
||||||
|
onClose={goToAuthClientsPage}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -44,8 +44,8 @@ export default function CreateRole(): React.ReactElement {
|
|||||||
enqueueSnackbar(formatMessage('createRole.successfullyCreated'), {
|
enqueueSnackbar(formatMessage('createRole.successfullyCreated'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-create-role-success'
|
'data-test': 'snackbar-create-role-success',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(URLS.ROLES);
|
navigate(URLS.ROLES);
|
||||||
@@ -58,7 +58,9 @@ export default function CreateRole(): React.ReactElement {
|
|||||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
<Grid container item xs={12} sm={10} md={9}>
|
<Grid container item xs={12} sm={10} md={9}>
|
||||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||||
<PageTitle>{formatMessage('createRolePage.title')}</PageTitle>
|
<PageTitle data-test="create-role-title">
|
||||||
|
{formatMessage('createRolePage.title')}
|
||||||
|
</PageTitle>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
|
@@ -50,7 +50,7 @@ export default function CreateUser(): React.ReactElement {
|
|||||||
persist: true,
|
persist: true,
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-create-user-success',
|
'data-test': 'snackbar-create-user-success',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(URLS.USERS);
|
navigate(URLS.USERS);
|
||||||
@@ -63,7 +63,9 @@ export default function CreateUser(): React.ReactElement {
|
|||||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
<Grid container item xs={12} sm={10} md={9}>
|
<Grid container item xs={12} sm={10} md={9}>
|
||||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||||
<PageTitle>{formatMessage('createUserPage.title')}</PageTitle>
|
<PageTitle data-test="create-user-title">
|
||||||
|
{formatMessage('createUserPage.title')}
|
||||||
|
</PageTitle>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
|
@@ -54,8 +54,8 @@ export default function EditRole(): React.ReactElement {
|
|||||||
enqueueSnackbar(formatMessage('editRole.successfullyUpdated'), {
|
enqueueSnackbar(formatMessage('editRole.successfullyUpdated'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-edit-role-success'
|
'data-test': 'snackbar-edit-role-success',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(URLS.ROLES);
|
navigate(URLS.ROLES);
|
||||||
@@ -70,7 +70,9 @@ export default function EditRole(): React.ReactElement {
|
|||||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
<Grid container item xs={12} sm={10} md={9}>
|
<Grid container item xs={12} sm={10} md={9}>
|
||||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||||
<PageTitle>{formatMessage('editRolePage.title')}</PageTitle>
|
<PageTitle data-test="edit-role-title">
|
||||||
|
{formatMessage('editRolePage.title')}
|
||||||
|
</PageTitle>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
|
@@ -57,8 +57,8 @@ export default function EditUser(): React.ReactElement {
|
|||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-edit-user-success',
|
'data-test': 'snackbar-edit-user-success',
|
||||||
persist: true
|
persist: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(URLS.USERS);
|
navigate(URLS.USERS);
|
||||||
@@ -71,7 +71,9 @@ export default function EditUser(): React.ReactElement {
|
|||||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
<Grid container item xs={12} sm={10} md={9}>
|
<Grid container item xs={12} sm={10} md={9}>
|
||||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||||
<PageTitle>{formatMessage('editUserPage.title')}</PageTitle>
|
<PageTitle data-test="edit-user-title">
|
||||||
|
{formatMessage('editUserPage.title')}
|
||||||
|
</PageTitle>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
|
@@ -18,16 +18,12 @@ function RolesPage() {
|
|||||||
<Grid container item xs={12} sm={10} md={9}>
|
<Grid container item xs={12} sm={10} md={9}>
|
||||||
<Grid container sx={{ mb: [0, 3] }} columnSpacing={1.5} rowSpacing={3}>
|
<Grid container sx={{ mb: [0, 3] }} columnSpacing={1.5} rowSpacing={3}>
|
||||||
<Grid container item xs sm alignItems="center">
|
<Grid container item xs sm alignItems="center">
|
||||||
<PageTitle>{formatMessage('rolesPage.title')}</PageTitle>
|
<PageTitle data-test="roles-page-title">
|
||||||
|
{formatMessage('rolesPage.title')}
|
||||||
|
</PageTitle>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid
|
<Grid container item xs="auto" sm="auto" alignItems="center">
|
||||||
container
|
|
||||||
item
|
|
||||||
xs="auto"
|
|
||||||
sm="auto"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<ConditionalIconButton
|
<ConditionalIconButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@@ -18,16 +18,12 @@ function UsersPage() {
|
|||||||
<Grid container item xs={12} sm={10} md={9}>
|
<Grid container item xs={12} sm={10} md={9}>
|
||||||
<Grid container sx={{ mb: [0, 3] }} columnSpacing={1.5} rowSpacing={3}>
|
<Grid container sx={{ mb: [0, 3] }} columnSpacing={1.5} rowSpacing={3}>
|
||||||
<Grid container item xs sm alignItems="center">
|
<Grid container item xs sm alignItems="center">
|
||||||
<PageTitle>{formatMessage('usersPage.title')}</PageTitle>
|
<PageTitle data-test="users-page-title">
|
||||||
|
{formatMessage('usersPage.title')}
|
||||||
|
</PageTitle>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid
|
<Grid container item xs="auto" sm="auto" alignItems="center">
|
||||||
container
|
|
||||||
item
|
|
||||||
xs="auto"
|
|
||||||
sm="auto"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<ConditionalIconButton
|
<ConditionalIconButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
34
yarn.lock
34
yarn.lock
@@ -6054,13 +6054,6 @@ axios-retry@^3.2.4:
|
|||||||
"@babel/runtime" "^7.15.4"
|
"@babel/runtime" "^7.15.4"
|
||||||
is-retry-allowed "^2.2.0"
|
is-retry-allowed "^2.2.0"
|
||||||
|
|
||||||
axios@0.24.0:
|
|
||||||
version "0.24.0"
|
|
||||||
resolved "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz"
|
|
||||||
integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
|
|
||||||
dependencies:
|
|
||||||
follow-redirects "^1.14.4"
|
|
||||||
|
|
||||||
axios@0.26.0:
|
axios@0.26.0:
|
||||||
version "0.26.0"
|
version "0.26.0"
|
||||||
resolved "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz"
|
resolved "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz"
|
||||||
@@ -6068,6 +6061,15 @@ axios@0.26.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects "^1.14.8"
|
follow-redirects "^1.14.8"
|
||||||
|
|
||||||
|
axios@1.6.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102"
|
||||||
|
integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.15.0"
|
||||||
|
form-data "^4.0.0"
|
||||||
|
proxy-from-env "^1.1.0"
|
||||||
|
|
||||||
axobject-query@^2.2.0:
|
axobject-query@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz"
|
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz"
|
||||||
@@ -9580,15 +9582,10 @@ fn.name@1.x.x:
|
|||||||
resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz"
|
||||||
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
||||||
|
|
||||||
follow-redirects@^1.0.0, follow-redirects@^1.14.4:
|
follow-redirects@^1.0.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0:
|
||||||
version "1.14.8"
|
version "1.15.3"
|
||||||
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
|
||||||
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
|
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
|
||||||
|
|
||||||
follow-redirects@^1.14.8:
|
|
||||||
version "1.14.9"
|
|
||||||
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz"
|
|
||||||
integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
|
|
||||||
|
|
||||||
forever-agent@~0.6.1:
|
forever-agent@~0.6.1:
|
||||||
version "0.6.1"
|
version "0.6.1"
|
||||||
@@ -15289,6 +15286,11 @@ proxy-addr@~2.0.7:
|
|||||||
forwarded "0.2.0"
|
forwarded "0.2.0"
|
||||||
ipaddr.js "1.9.1"
|
ipaddr.js "1.9.1"
|
||||||
|
|
||||||
|
proxy-from-env@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||||
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
psl@^1.1.28, psl@^1.1.33:
|
psl@^1.1.28, psl@^1.1.33:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz"
|
resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz"
|
||||||
|
Reference in New Issue
Block a user