// ParseServer - open-source compatible API Server for Parse apps var batch = require('./batch'), bodyParser = require('body-parser'), DatabaseAdapter = require('./DatabaseAdapter'), express = require('express'), middlewares = require('./middlewares'), multer = require('multer'), Parse = require('parse/node').Parse, path = require('path'), authDataManager = require('./authDataManager'); if (!global._babelPolyfill) { require('babel-polyfill'); } import { logger, configureLogger } from './logger'; import AppCache from './cache'; import Config from './Config'; import parseServerPackage from '../package.json'; import PromiseRouter from './PromiseRouter'; import requiredParameter from './requiredParameter'; import { AnalyticsRouter } from './Routers/AnalyticsRouter'; import { ClassesRouter } from './Routers/ClassesRouter'; import { FeaturesRouter } from './Routers/FeaturesRouter'; import { InMemoryCacheAdapter } from './Adapters/Cache/InMemoryCacheAdapter'; import { CacheController } from './Controllers/CacheController'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { FilesController } from './Controllers/FilesController'; import { FilesRouter } from './Routers/FilesRouter'; import { FunctionsRouter } from './Routers/FunctionsRouter'; import { GlobalConfigRouter } from './Routers/GlobalConfigRouter'; import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; import { HooksController } from './Controllers/HooksController'; import { HooksRouter } from './Routers/HooksRouter'; import { IAPValidationRouter } from './Routers/IAPValidationRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter'; import { loadAdapter } from './Adapters/AdapterLoader'; import { LiveQueryController } from './Controllers/LiveQueryController'; import { LoggerController } from './Controllers/LoggerController'; import { LogsRouter } from './Routers/LogsRouter'; import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer'; import { PublicAPIRouter } from './Routers/PublicAPIRouter'; import { PushController } from './Controllers/PushController'; import { PushRouter } from './Routers/PushRouter'; import { randomString } from './cryptoUtils'; import { RolesRouter } from './Routers/RolesRouter'; import { SchemasRouter } from './Routers/SchemasRouter'; import { SessionsRouter } from './Routers/SessionsRouter'; import { UserController } from './Controllers/UserController'; import { UsersRouter } from './Routers/UsersRouter'; import { PurgeRouter } from './Routers/PurgeRouter'; import DatabaseController from './Controllers/DatabaseController'; const SchemaController = require('./Controllers/SchemaController'); import ParsePushAdapter from 'parse-server-push-adapter'; import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } }; // ParseServer works like a constructor of an express app. // The args that we understand are: // "filesAdapter": a class like GridStoreAdapter providing create, get, // and delete // "loggerAdapter": a class like FileLoggerAdapter providing info, error, // and query // "jsonLogs": log as structured JSON objects // "databaseURI": a uri like mongodb://localhost:27017/dbname to tell us // what database this Parse API connects to. // "cloud": relative location to cloud code to require, or a function // that is given an instance of Parse as a parameter. Use this instance of Parse // to register your cloud code hooks and functions. // "appId": the application id to host // "masterKey": the master key for requests to this app // "facebookAppIds": an array of valid Facebook Application IDs, required // if using Facebook login // "collectionPrefix": optional prefix for database collection names // "fileKey": optional key from Parse dashboard for supporting older files // hosted by Parse // "clientKey": optional key from Parse dashboard // "dotNetKey": optional key from Parse dashboard // "restAPIKey": optional key from Parse dashboard // "webhookKey": optional key from Parse dashboard // "javascriptKey": optional key from Parse dashboard // "push": optional key from configure push // "sessionLength": optional length in seconds for how long Sessions should be valid for class ParseServer { constructor({ appId = requiredParameter('You must provide an appId!'), masterKey = requiredParameter('You must provide a masterKey!'), appName, filesAdapter, push, loggerAdapter, jsonLogs, logsFolder, databaseURI, databaseOptions, databaseAdapter, cloud, collectionPrefix = '', clientKey, javascriptKey, dotNetKey, restAPIKey, webhookKey, fileKey = undefined, facebookAppIds = [], enableAnonymousUsers = true, allowClientClassCreation = true, oauth = {}, serverURL = requiredParameter('You must provide a serverURL!'), maxUploadSize = '20mb', verifyUserEmails = false, preventLoginWithUnverifiedEmail = false, emailVerifyTokenValidityDuration, cacheAdapter, emailAdapter, publicServerURL, customPages = { invalidLink: undefined, verifyEmailSuccess: undefined, choosePassword: undefined, passwordResetSuccess: undefined }, liveQuery = {}, sessionLength = 31536000, // 1 Year in seconds expireInactiveSessions = true, verbose = false, revokeSessionOnPasswordReset = true, __indexBuildCompletionCallbackForTests = () => {}, }) { // Initialize the node client SDK automatically Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.serverURL = serverURL; if ((databaseOptions || databaseURI || collectionPrefix !== '') && databaseAdapter) { throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/connectionPrefix.'; } else if (!databaseAdapter) { databaseAdapter = new MongoStorageAdapter({ uri: databaseURI, collectionPrefix, mongoOptions: databaseOptions, }); } else { databaseAdapter = loadAdapter(databaseAdapter) } if (!filesAdapter && !databaseURI) { throw 'When using an explicit database adapter, you must also use and explicit filesAdapter.'; } if (logsFolder) { configureLogger({logsFolder, jsonLogs}); } if (cloud) { addParseCloud(); if (typeof cloud === 'function') { cloud(Parse) } else if (typeof cloud === 'string') { require(path.resolve(process.cwd(), cloud)); } else { throw "argument 'cloud' must either be a string or a function"; } } if (verbose || process.env.VERBOSE || process.env.VERBOSE_PARSE_SERVER) { configureLogger({level: 'silly', jsonLogs}); } const filesControllerAdapter = loadAdapter(filesAdapter, () => { return new GridStoreAdapter(databaseURI); }); // Pass the push options too as it works with the default const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push || {}); const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); const emailControllerAdapter = loadAdapter(emailAdapter); const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId}); // We pass the options and the base class for the adatper, // Note that passing an instance would work too const filesController = new FilesController(filesControllerAdapter, appId); const pushController = new PushController(pushControllerAdapter, appId, push); const loggerController = new LoggerController(loggerControllerAdapter, appId); const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails }); const liveQueryController = new LiveQueryController(liveQuery); const cacheController = new CacheController(cacheControllerAdapter, appId); const databaseController = new DatabaseController(databaseAdapter); const hooksController = new HooksController(appId, databaseController, webhookKey); // TODO: create indexes on first creation of a _User object. Otherwise it's impossible to // have a Parse app without it having a _User collection. let userClassPromise = databaseController.loadSchema() .then(schema => schema.enforceClassExists('_User')) let usernameUniqueness = userClassPromise .then(() => databaseController.adapter.ensureUniqueness('_User', requiredUserFields, ['username'])) .catch(error => { logger.warn('Unable to ensure uniqueness for usernames: ', error); return Promise.reject(error); }); let emailUniqueness = userClassPromise .then(() => databaseController.adapter.ensureUniqueness('_User', requiredUserFields, ['email'])) .catch(error => { logger.warn('Unabled to ensure uniqueness for user email addresses: ', error); return Promise.reject(error); }) AppCache.put(appId, { appId, masterKey: masterKey, serverURL: serverURL, collectionPrefix: collectionPrefix, clientKey: clientKey, javascriptKey: javascriptKey, dotNetKey: dotNetKey, restAPIKey: restAPIKey, webhookKey: webhookKey, fileKey: fileKey, facebookAppIds: facebookAppIds, cacheController: cacheController, filesController: filesController, pushController: pushController, loggerController: loggerController, hooksController: hooksController, userController: userController, verifyUserEmails: verifyUserEmails, preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail, emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration, allowClientClassCreation: allowClientClassCreation, authDataManager: authDataManager(oauth, enableAnonymousUsers), appName: appName, publicServerURL: publicServerURL, customPages: customPages, maxUploadSize: maxUploadSize, liveQueryController: liveQueryController, sessionLength: Number(sessionLength), expireInactiveSessions: expireInactiveSessions, jsonLogs, revokeSessionOnPasswordReset, databaseController, }); // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability if (process.env.FACEBOOK_APP_ID) { AppCache.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } Config.validate(AppCache.get(appId)); this.config = AppCache.get(appId); hooksController.load(); // Note: Tests will start to fail if any validation happens after this is called. if (process.env.TESTING) { __indexBuildCompletionCallbackForTests(Promise.all([usernameUniqueness, emailUniqueness])); } } get app() { return ParseServer.app(this.config); } static app({maxUploadSize = '20mb', appId}) { // This app serves the Parse API directly. // It's the equivalent of https://api.parse.com/1 in the hosted Parse API. var api = express(); //api.use("/apps", express.static(__dirname + "/public")); // File handling needs to be before default middlewares are applied api.use('/', middlewares.allowCrossDomain, new FilesRouter().getExpressRouter({ maxUploadSize: maxUploadSize })); api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressApp()); // TODO: separate this from the regular ParseServer object if (process.env.TESTING == 1) { api.use('/', require('./testing-routes').router); } api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize })); api.use(middlewares.allowCrossDomain); api.use(middlewares.allowMethodOverride); api.use(middlewares.handleParseHeaders); let routers = [ new ClassesRouter(), new UsersRouter(), new SessionsRouter(), new RolesRouter(), new AnalyticsRouter(), new InstallationsRouter(), new FunctionsRouter(), new SchemasRouter(), new PushRouter(), new LogsRouter(), new IAPValidationRouter(), new FeaturesRouter(), new GlobalConfigRouter(), new PurgeRouter(), ]; if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) { routers.push(new HooksRouter()); } let routes = routers.reduce((memo, router) => { return memo.concat(router.routes); }, []); let appRouter = new PromiseRouter(routes, appId); batch.mountOnto(appRouter); api.use(appRouter.expressApp()); api.use(middlewares.handleParseErrors); //This causes tests to spew some useless warnings, so disable in test if (!process.env.TESTING) { process.on('uncaughtException', (err) => { if ( err.code === "EADDRINUSE" ) { // user-friendly message for this common error console.error(`Unable to listen on port ${err.port}. The port is already in use.`); process.exit(0); } else { throw err; } }); } return api; } static createLiveQueryServer(httpServer, config) { return new ParseLiveQueryServer(httpServer, config); } } function addParseCloud() { const ParseCloud = require("./cloud-code/Parse.Cloud"); Object.assign(Parse.Cloud, ParseCloud); global.Parse = Parse; } export default ParseServer;