Compare commits

...

1 Commits

Author SHA1 Message Date
Rıdvan Akca
a650e3beaa feat(amazon-s3): add amazon s3 integration 2023-11-21 16:28:47 +03:00
11 changed files with 293 additions and 5 deletions

View 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

View 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,
};

View File

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

View File

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

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

View File

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

View File

@@ -1,9 +1,7 @@
import { IGlobalVariable } from '@automatisch/types';
const getCurrentUser = async ($: IGlobalVariable) => {
const { data: currentUser } = await $.http.get(
'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses'
);
const { data: currentUser } = await $.http.get('/');
return currentUser;
};

View File

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

View File

@@ -1,5 +1,5 @@
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 currentUser = await getCurrentUser($);

View File

@@ -1,5 +1,5 @@
import { IField, IGlobalVariable } from '@automatisch/types';
import getCurrentUser from '../common/get-current-user';
import getCurrentUser from '../../amazon-s3/common/get-current-user';
type TUser = {
displayName: string;