UNPKG

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

322 lines (283 loc) 9.24 kB
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 } }