index.js

const axios = require('axios');
const crypto = require('crypto');

const options = {
    baseURL: 'https://blih.epitech.eu/',
    timeout: 10000
};

/*
 * Interceptors
 */
function responseDataInterceptor (response) {
    return response;
}

function responseErrorInterceptor (error) {
    if (error.response && error.response.data.error) {
        return Promise.reject(error.response.data.error);
    } else {
        return Promise.reject(error.message);
    }
}

// Always use Node.js adapter
axios.defaults.adapter = require('axios/lib/adapters/http');

/**
 * Blih API
 * @class Blih
 */
class Blih {

    constructor (credentials) {
        if (!credentials) {
            throw 'Missing credentials';
        } else if (!credentials.email) {
            throw 'Email is mandatory to authenticate';
        } else if (!credentials.password && !credentials.token) {
            throw 'A password or token is needed to authenticate';
        }

        /**
         * Email of the user
         * @type {String}
         */
        this.email = credentials.email;
        /**
         * Token to use when communicating with the API
         * @type {String}
         */
        this.token = credentials.token ? credentials.token :
            crypto.createHash('sha512').update(credentials.password).digest('hex');

        this.api = axios.create(options);
        this.api.interceptors.response.use(responseDataInterceptor, responseErrorInterceptor);
    }

    /**
     * Create a repository
     * @param  {String} repository  Name of the new repository
     * @param  {String} description A short description of the repository
     * @return {Promise} description
     */
    async createRepository (repository, description) {
        const data = {
            name: repository,
            type: 'git',
            description
        };

        return (await this.call('post', '/repositories', data)).data.message;
    }

    /**
     * Delete a repository
     * @async
     * @param  {String} repository Name of the repository to delete
     * @return {Promise} description
     */
    async deleteRepository (repository) {
        repository = encodeURIComponent(repository);
        return (await this.call('delete', `/repository/${repository}`)).data.message;
    }

    /**
     * A brief summary of a repository
     * @typedef {Object} Blih~PartialRepository
     * @property {String} name - The name of the repository
     * @property {String} url - The URL that the API uses for this repository
     * @property {String} uuid - UUID of the repository
     */

    /**
     * List repositories
     * @async
     * @return {PartialRepository[]} the repositories you own
     */
    async listRepositories () {
        const list = (await this.call('get', '/repositories')).data.repositories;
        return Object.keys(list)
            .filter(r => r.length).sort()
            .map(r => ({
                name: r,
                url: list[r].url,
                uuid: list[r].uuid
            }));
    }

    /**
     * A detailed summary of a repository
     * @typedef {Object} Blih~Repository
     * @property {String} name - The name of the repository
     * @property {String} url - The URL that the API uses for this repository
     * @property {Number} creation_time - POSIX creation time of the repository
     * @property {String} uuid - UUID of the repository
     * @property {String} description - UUID of the repository
     * @property {boolean} public - Visibility of the repository
     */

    /**
     * Get information about a repository
     * @async
     * @return {Repository} information about the repository
     */
    async repositoryInfo (repository) {
        repository = encodeURIComponent(repository);
        const info = (await this.call('get', `/repository/${repository}`)).data.message;
        info.name = repository;
        info.creation_time = Number(info.creation_time);
        info.public = (info.public !== 'False');
        return info;
    }

    /**
     * A collaborator associated with their rights on a repository
     * @typedef {Object} Blih~ACL
     * @property {String} name - Name of the collaborator
     * @property {String} rights - one or many of 'a' (admin), 'r' (read) or 'w' (write)
     */

    /**
    * Get ACL of a repository
    * @async
    * @return {ACL[]} the collaborators on this repository
    */
    async getACL (repository) {
        repository = encodeURIComponent(repository);
        try {
            const acl = (await this.call('get', `/repository/${repository}/acls`)).data;
            return Object.keys(acl)
                .filter(c => c.length && acl[c].length).sort()
                .map(c => ({
                    name: c,
                    rights: acl[c]
                }));
        } catch (e) {
            if (e === 'No ACLs') {
                return [];
            } else {
                throw e;
            }
        }
    }

    /**
    * Set ACL for a repository
    * @async
    * @param {String} repository
    * @param {String} user
    * @param {String} acl - one or many of 'a' (admin), 'r' (read) or 'w' (write)
    * @return {String} a message confirming ACL update
    */
    async setACL (repository, user, acl) {
        const data = {
            acl,
            user
        };

        repository = encodeURIComponent(repository);
        return (await this.call('post', `/repository/${repository}/acls`, data)).data.message;
    }

    /**
     * Upload an SSH key. Only RSA keys are supported.
     * @param  {String} key - key contents (NOT the path to the file)
     * @return {String} a message confirming upload
     */
    async uploadKey (key) {
        const data = {
            sshkey: key
        };

        return (await this.call('post', '/sshkeys', data)).data.message;
    }

    /**
     * Delete an SSH key
     * @param  {String} key - name of the key (usually corresponds to the key comment)
     * @return {String} a message confirming deletion
     */
    async deleteKey (key) {
        key = encodeURIComponent(key);
        return (await this.call('delete', `/sshkey/${key}`)).data.message;
    }

    /**
     * An SSH key
     * @typedef {Object} Blih~Key
     * @property {String} name - Name identifying the key (usually corresponds to the key comment)
     * @property {String} data - Actual key contents
     */

    /**
     * List all SSH keys
     * @return {Key[]} the public keys associated with your account
     */
    async listKeys () {
        const keys = (await this.call('get', '/sshkeys')).data;
        return Object.keys(keys)
            .filter(k => k.length).sort()
            .map(k => ({
                name: k,
                data: keys[k]
            }));
    }

    /**
     * Get your legacy identity
     * This is only useful for accounts created prior to 2016 that used the old login format
     * For newer users, this will simply return their email.
     * @return {String} the public keys associated with your account
     */
    async whoami () {
        return (await this.call('get', '/whoami')).data.message;
    }

    /**
     * Ping the Blih server
     * @return {Number} the response time in milliseconds
     */
    static async ping () {
        const api = axios.create(options);

        // Add timestamps to requests and responses
        api.interceptors.request.use(config => {
            config.startTimestamp = Date.now();
            return config;
        }, error => Promise.reject(error));
        api.interceptors.response.use(response => {
            response.config.endTimestamp = Date.now();
            return response;
        }, responseErrorInterceptor);

        const res = await api.get('/');
        return res.config.endTimestamp - res.config.startTimestamp;
    }

    /**
     * Make a generic call to the Blih API
     * @private
     * @param  {String} method - HTTP method to use
     * @param  {String} endpoint - remote endpoint to use
     * @param  {Object} data - request body additionnal data
     * @return {Promise} the request
     */
    async call (method, endpoint, data) {
        let body = { user: this.email, data };

        body.signature = crypto.createHmac('sha512', this.token)
            .update(body.user)
            .update(body.data ? JSON.stringify(body.data, null, 4) : '')
            .digest('hex');

        return this.api.request({
            method,
            url: endpoint,
            data: body
        });
    }

}

module.exports = Blih;