0.6.1 • Published 2 years ago

@crowdin/app-project-module v0.6.1

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

Crowdin App Project Module

Module that will automatically add all necessary endpoints for Crowdin App.

It either can extends your Express application or even create it for you.

Module expose two main methods:

  • createApp to fully create for you Express application
  • addCrowdinEndpoints to extend your Express application

In both options you will need to provide Crowdin App configuration file. Please refer to jsdoc for more details.

Status

npm Build Status npm

Table of Contents

Installation

npm

npm i @crowdin/app-project-module

yarn

yarn add @crowdin/app-project-module

Sample app

const crowdinModule = require('@crowdin/app-project-module');
const crowdinAppFunctions = require('@crowdin/crowdin-apps-functions');
const axios = require('axios').default;

const configuration = {
    baseUrl: 'https://123.ngrok.io',
    clientId: 'clientId',
    clientSecret: 'clientSecret',
    name: 'Sample App',
    identifier: 'sample-app',
    description: 'Sample App description',
    dbFolder: __dirname,
    imagePath: __dirname + '/' + 'logo.png',
    integration: {
        withRootFolder: true,
        getIntegrationFiles: async (credentials, appSettings) => {
            //here you need to fetch files/objects from integration
            return [
                {
                    id: '12',
                    name: 'File from integration',
                    type: 'json',
                    parentId: '10'
                },
                {
                    id: '14',
                    name: 'File from integration2',
                    type: 'xml',
                    parentId: '11'
                },
                {
                    id: '10',
                    name: 'Folder from integration'
                },
                {
                    id: '11',
                    name: 'Folder from integratio2'
                },
            ];
        },
        updateCrowdin: async (projectId, client, credentials, request, rootFolder, appSettings) => {
            //here you need to get data from integration and upload it to Crowdin
            console.log(`Request for updating data in Crowdin ${JSON.stringify(request)}`);
            const directories = await client.sourceFilesApi
                .withFetchAll()
                .listProjectDirectories(projectId);
            const { folder, files } = await crowdinAppFunctions.getOrCreateFolder(
                directories.data.map((d) => d.data),
                client,
                projectId,
                'Folder from integration',
                rootFolder
            );
            const fileContent = {
                title: 'Hello World',
            };
            await crowdinAppFunctions.updateOrCreateFile(
                client,
                projectId,
                'integration.json',
                'Sample file from integration',
                'json',
                folder.id,
                fileContent,
                files.find((f) => f.name === 'integration.json'),
            );
        },
        updateIntegration: async (projectId, client, credentials, request, rootFolder, appSettings) => {
            ////here should be logic to get translations from Crowdin and upload them to integration
            console.log(`Request for updating data in Integration ${JSON.stringify(request)}`);
            const directories = await client.sourceFilesApi
                .withFetchAll()
                .listProjectDirectories(projectId);
            const { files } = await crowdinAppFunctions.getFolder(
                directories.data.map((d) => d.data),
                client,
                projectId,
                'Folder from integration',
                rootFolder
            );
            const file = files.find((f) => f.name === 'integration.json');
            if (file) {
                const translationsLink =
                    await client.translationsApi.buildProjectFileTranslation(
                        projectId,
                        file.id,
                        { targetLanguageId: 'uk' },
                    );
                if (!translationsLink) {
                    return;
                }
                const response = await axios.get(translationsLink.data.url);
                console.log(response.data);
            }
        },
    }
};

crowdinModule.createApp(configuration);

Customize your app login form

By default login page for your app will require only to enter apiToken to communicate with third party service.
But there is also a possibility to customize it.

configuration.loginForm = {
    fields: [
        {
            key: 'username',
            label: 'Username',
        },
        {
            key: 'password',
            label: 'Password',
            type: 'password'
        },
        {
            helpText: 'Api Key for http requests',
            key: 'apiKey',
            label: 'Api Key'
        }
    ]
};

OAuth2 support

In case if third party service uses OAuth2 for authorization use oauthLogin field to configure it.
loginForm in this case should remain undefined.

Github example:

configuration.oauthLogin = {
    authorizationUrl: 'https://github.com/login/oauth/authorize',
    clientId: 'github_app_client_id',
    clientSecret: 'github_app_client_secret',
    accessTokenUrl: 'https://github.com/login/oauth/access_token'
}

Google example:

configuration.oauthLogin = {
    scope: 'https%3A//www.googleapis.com/auth/userinfo.email',
    authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
    clientId: 'google_web_app_client_id',
    clientSecret: 'google_web_app_client_secret',
    accessTokenUrl: 'https://oauth2.googleapis.com/token',
    extraAutorizationUrlParameters: {
        response_type: 'code',
        access_type: 'offline',
        prompt: 'consent'
    },
    extraAccessTokenParameters: {
        grant_type: 'authorization_code'
    },
    extraRefreshTokenParameters: {
        grant_type: 'refresh_token'
    },
    refresh: true
}

The oauthLogin property allows you to customize many different properties, mappings, etc. So that you can integrate with any OAuth2 implementation.
Main default values:

  • redirect uri prefix will be /oauth/code
  • client id field name in url parameters and in request payload will be client_id
  • client secret field name in request payload will be client_secret
  • access token field name should be access_token
  • be default assumption is that token do not have any expiration date, to change this behavior use refresh flag so then refresh token and expires in will be taken into consideration
  • access token field name should be refresh_token
  • expires in field name should be expires_in (value should be in seconds)

This module rely that OAuth2 protocol is implemented by third party service in this way:

  • request for access token should be done via POST request to accessTokenUrl with JSON body that will contain at least clientId, clientSecret, code and redirectUri (also possible to add extra fields via extraAccessTokenParameters property)
  • request to refresh token should be done via POST request to accessTokenUrl (or refreshTokenUrl if definied) with JSON body that will contain at least clientId, clientSecret and refreshToken (also possible to add extra fields via extraRefreshTokenParameters property)
  • both requests will return JSON response with body that contains accessToken and, if enabled, refreshToken (optional) and expireIn

To override those requests please use performGetTokenRequest and performRefreshTokenRequest (e.g. when requests should be done with different HTTP methods or data should be tranfered as query string or form data).

Mailup example:

const clientId = 'client_id';
const clientSecret = 'client_secret';
const tokenUrl = 'https://services.mailup.com/Authorization/OAuth/Token';

configuration.oauthLogin = {
    authorizationUrl: 'https://services.mailup.com/Authorization/OAuth/LogOn',
    clientId,
    clientSecret,
    extraAutorizationUrlParameters: {
        response_type: 'code'
    },
    refresh: true,
    performGetTokenRequest: async (code) => {
        const url = `${tokenUrl}?code=${code}&grant_type=authorization_code`;
        const headers = {
            'Authorization': `Bearer ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`
        };
        return (await axios.get(url, { headers })).data;
    },
    performRefreshTokenRequest: async (credentials) => {
        const params = {
            refresh_token: credentials.refreshToken,
            grant_type: 'refresh_token',
            client_id: clientId,
            client_secret: clientSecret
        };
        const data = Object.keys(params)
            .map((key) => `${key}=${encodeURIComponent(params[key])}`)
            .join('&');
        const headers = {
            'Content-Type': 'application/x-www-form-urlencoded'
        };
        return (await axios.post(tokenUrl, data, { headers })).data;
    }
}

Please refer to jsdoc for more details.

Settings window

It is also possible to define settings window for your app where users can customize integration flow.

configuration.integration.getConfiguration = (projectId, crowdinClient, integrationCredentials) => {
    return [
        {
            key: 'flag',
            label: 'Checkbox',
            type: 'checkbox'
        },
        {
            key: 'text',
            label: 'Text input',
            type: 'text',
            helpText: 'Help text'
        },
        {
            key: 'option',
            label: 'Select',
            type: 'select',
            options: [
                {
                    value: '12',
                    label: 'Option'
                }
            ]
        }
    ]
}

Info window

You also can define section with some information notes or help section for your app.

configuration.integration.infoModal = {
    title: 'Info',
    content: `
        <h1>This is your app help section</h1>
        </br>
        <h2>This is just an example</h2>
    `
}

Background tasks

In order to register background tasks that app will invoke periodically invoke you can use cronJobs field.

configuration.integration.cronJobs = [
    {
        //every 10 seconds
        expression: '*/10 * * * * *',
        task: (projectId, client, apiCredentials, appRootFolder, config) => {
            console.log(`Running background task for project : ${projectId}`);
            console.log(`Api credentials : ${JSON.stringify(apiCredentials)}`);
            console.log(`App config : ${JSON.stringify(config)}`);
            console.log(appRootFolder ? JSON.stringify(appRootFolder) : 'No root folder');
        }
    }
]

For cron syntax guide please refer to this documentation.

Error propagation

In case if something is wrong with app settings or credentials are invalid you can throw an explanation message that will be then visible on the UI side.
e.g. check if entered credentials are valid:

configuration.integration.checkConnection = (credentials) => {
    if (!credentials.password  || credentials.password.length < 6) {
        throw 'Password is too weak';
    }
    //or call an service API with those credentials and check if request will be successful
};

Contributing

If you want to contribute please read the Contributing guidelines.

Seeking Assistance

If you find any problems or would like to suggest a feature, please feel free to file an issue on Github at Issues Page.

If you've found an error in these samples, please Contact Customer Success Service.

License