@uppy/companion
Version:
OAuth helper and remote fetcher for Uppy's (https://uppy.io) extensible file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox and Google Drive, S3 and more :dog:
173 lines (172 loc) • 9.33 kB
JavaScript
import { randomUUID } from 'node:crypto';
import cookieParser from 'cookie-parser';
import express from 'express';
import interceptor from 'express-interceptor';
import grant from 'grant';
import merge from 'lodash/merge.js';
import packageJson from '../package.json' with { type: 'json' };
import { defaultOptions, getMaskableSecrets, validateConfig, } from './config/companion.js';
import grantConfigFn from './config/grant.js';
import googlePicker from './server/controllers/googlePicker.js';
import * as controllers from './server/controllers/index.js';
import s3 from './server/controllers/s3.js';
import searchController from './server/controllers/search.js';
import url from './server/controllers/url.js';
import createEmitter from './server/emitter/index.js';
import { getURLBuilder } from './server/helpers/utils.js';
import * as jobs from './server/jobs.js';
import logger from './server/logger.js';
import * as middlewares from './server/middlewares.js';
import { getCredentialsOverrideMiddleware } from './server/provider/credentials.js';
import { ProviderApiError, ProviderAuthError, ProviderUserError, } from './server/provider/error.js';
import * as providerManager from './server/provider/index.js';
import { isOAuthProvider } from './server/provider/Provider.js';
import * as redis from './server/redis.js';
import socket from './server/socket.js';
export { socket };
const grantConfig = grantConfigFn();
export function setLoggerProcessName({ loggerProcessName }) {
if (loggerProcessName != null)
logger.setProcessName(loggerProcessName);
}
// intercepts grantJS' default response error when something goes
// wrong during oauth process.
const interceptGrantErrorResponse = interceptor((req, res) => {
return {
isInterceptable: () => {
// match grant.js' callback url
return /^\/connect\/\w+\/callback/.test(req.path);
},
intercept: (body, send) => {
const unwantedBody = 'error=Grant%3A%20missing%20session%20or%20misconfigured%20provider';
if (body === unwantedBody) {
logger.error(`grant.js responded with error: ${body}`, 'grant.oauth.error', req.id);
res.set('Content-Type', 'text/plain');
const reqHint = req.id ? `Request ID: ${req.id}` : '';
send([
'Companion was unable to complete the OAuth process :(',
'Error: User session is missing or the Provider was misconfigured',
reqHint,
].join('\n'));
}
else {
send(body);
}
},
};
});
// make the errors available publicly for custom providers
export const errors = {
ProviderApiError,
ProviderUserError,
ProviderAuthError,
};
/**
* Entry point into initializing the Companion app.
*
* @param {object} optionsArg
* @returns {{ app: import('express').Express, emitter: any }}}
*/
export function app(optionsArg = {}) {
setLoggerProcessName(optionsArg);
validateConfig(optionsArg);
const options = merge({}, defaultOptions, optionsArg);
const providers = providerManager.getDefaultProviders();
const { customProviders } = options;
if (customProviders) {
providerManager.addCustomProviders(customProviders, providers, grantConfig);
}
const getOauthProvider = (providerName) => providers[providerName]?.oauthProvider;
providerManager.addProviderOptions(options, grantConfig, getOauthProvider);
// mask provider secrets from log messages
logger.setMaskables(getMaskableSecrets(options));
// create singleton redis client if corresponding options are set
const redisClient = redis.client(options);
const emitter = createEmitter(redisClient, options.redisPubSubScope);
const app = express();
if (options.metrics) {
app.use(middlewares.metrics({ path: options.server.path }));
}
app.use(cookieParser()); // server tokens are added to cookies
app.use(interceptGrantErrorResponse);
// override provider credentials at request time
// Making `POST` request to the `/connect/:provider/:override?` route requires a form body parser middleware:
// See https://github.com/simov/grant#dynamic-http
app.use('/connect/:oauthProvider/:override?', express.urlencoded({ extended: false }), getCredentialsOverrideMiddleware(providers, options));
app.use(grant.default.express(grantConfig));
app.use((req, res, next) => {
if (options.sendSelfEndpoint) {
const { protocol } = options.server;
res.header('i-am', `${protocol}://${options.sendSelfEndpoint}`);
}
next();
});
app.use(middlewares.cors(options));
// add uppy options to the request object so it can be accessed by subsequent handlers.
app.use('*', middlewares.getCompanionMiddleware(options));
app.use('/s3', s3(options.s3));
if (options.enableUrlEndpoint)
app.use('/url', url());
if (options.enableGooglePickerEndpoint)
app.use('/google-picker', googlePicker());
app.post('/:providerName/preauth', express.json(), express.urlencoded({ extended: false }), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasOAuthProvider, controllers.preauth);
app.get('/:providerName/connect', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.connect);
app.get('/:providerName/redirect', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.redirect);
app.get('/:providerName/callback', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.callback);
app.post('/:providerName/refresh-token', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.verifyToken, controllers.refreshToken);
app.post('/:providerName/deauthorization/callback', express.json(), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasOAuthProvider, controllers.deauthorizationCallback);
app.get('/:providerName/logout', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.gentleVerifyToken, controllers.logout);
app.get('/:providerName/send-token', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.verifyToken, controllers.sendToken);
app.post('/:providerName/simple-auth', express.json(), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasSimpleAuthProvider, controllers.simpleAuth);
app.get('/:providerName/list/:id?', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.list);
app.get('/:providerName/search', middlewares.hasSessionAndProvider, middlewares.verifyToken, searchController);
// backwards compat:
app.get('/search/:providerName/list', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.list);
app.post('/:providerName/get/:id', express.json(), middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.get);
// backwards compat:
app.post('/search/:providerName/get/:id', express.json(), middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.get);
app.get('/:providerName/thumbnail/:id', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, middlewares.cookieAuthToken, middlewares.verifyToken, controllers.thumbnail);
// Used for testing dynamic credentials only, normally this would run on a separate server.
if (options.testDynamicOauthCredentials) {
app.post('/:providerName/test-dynamic-oauth-credentials', (req, res) => {
if (req.query.secret !== options.testDynamicOauthCredentialsSecret)
throw new Error('Invalid secret');
const { providerName } = req.params;
// for simplicity, we just return the normal credentials for the provider, but in a real-world scenario,
// we would query based on parameters
const { key, secret } = options.providerOptions[providerName] ?? {
__proto__: null,
};
function getTransloaditGateway() {
const oauthProvider = getOauthProvider(providerName);
if (!isOAuthProvider(oauthProvider))
return undefined;
return getURLBuilder(options)('', true);
}
const response = {
credentials: {
key,
secret,
transloadit_gateway: getTransloaditGateway(),
origins: ['http://localhost:5173'],
},
};
logger.info(`Returning dynamic OAuth2 credentials for ${providerName}`, JSON.stringify(response));
res.send(response);
});
}
app.param('providerName', providerManager.getProviderMiddleware(providers, grantConfig));
if (app.get('env') !== 'test') {
jobs.startCleanUpJob(options.filePath);
}
const processId = randomUUID();
jobs.startPeriodicPingJob({
urls: options.periodicPingUrls,
interval: options.periodicPingInterval,
count: options.periodicPingCount,
staticPayload: options.periodicPingStaticPayload,
version: packageJson.version,
processId,
});
return { app, emitter };
}