diff --git a/.env b/.env index d7a2c743..1984051a 100644 --- a/.env +++ b/.env @@ -1,6 +1,8 @@ PARSE_SERVER_APPLICATION_ID=applicationId +PARSE_SERVER_MAINTENANCE_KEY=maintenanceKey PARSE_SERVER_PRIMARY_KEY=primaryKey PARSE_SERVER_WEBHOOK_KEY=webhookKey +POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres PG_PARSE_USER=parse PG_PARSE_PASSWORD=parse diff --git a/Package.resolved b/Package.resolved index 076a42c4..30cd9dc8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "0ae99db85b2b9d1e79b362bd31fd1ffe492f7c47", - "version" : "1.21.2" + "revision" : "333f51104b75d1a5b94cb3b99e4c58a3b442c9f7", + "version" : "1.25.2" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/async-kit.git", "state" : { - "revision" : "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31", - "version" : "1.20.0" + "revision" : "7ece208cd401687641c88367a00e3ea2b04311f1", + "version" : "1.19.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/netreconlab/Parse-Swift.git", "state" : { - "revision" : "b56de0a0770fb3ac267d3d8d1cc3924fbfbf3d16", - "version" : "5.11.2" + "revision" : "7a06604443662204d1d9a38a7307a8c42c8d1d6c", + "version" : "5.12.0" } }, { @@ -90,6 +90,24 @@ "version" : "3.5.2" } }, + { + "identity" : "swift-distributed-tracing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-distributed-tracing.git", + "state" : { + "revision" : "a64a0abc2530f767af15dd88dda7f64d5f1ff9de", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "d01361d32e14ae9b70ea5bd308a3794a198a2706", + "version" : "1.2.0" + } + }, { "identity" : "swift-http-types", "kind" : "remoteSourceControl", @@ -122,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "e4abde8be0e49dc7d66e6eed651254accdcd9533", - "version" : "2.69.0" + "revision" : "c51907a839e63ebf0ba2076bba73dd96436bd1b9", + "version" : "2.81.0" } }, { @@ -131,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "05c36b57453d23ea63785d58a7dbc7b70ba1745e", - "version" : "1.23.0" + "revision" : "00f3f72d2f9942d0e2dc96057ab50a37ced150d4", + "version" : "1.25.0" } }, { @@ -140,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "b5f7062b60e4add1e8c343ba4eb8da2e324b3a94", - "version" : "1.34.0" + "revision" : "a0224f3d20438635dd59c9fcc593520d80d131d0", + "version" : "1.33.0" } }, { @@ -149,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "2b09805797f21c380f7dc9bedaab3157c5508efb", - "version" : "2.27.0" + "revision" : "0cc3528ff48129d64ab9cab0b1cd621634edfc6b", + "version" : "2.29.3" } }, { @@ -171,13 +189,22 @@ "version" : "1.0.2" } }, + { + "identity" : "swift-service-context", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-service-context.git", + "state" : { + "revision" : "8946c930cae601452149e45d31d8ddfac973c3c7", + "version" : "1.2.0" + } + }, { "identity" : "swift-system", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "d2ba781702a1d8285419c15ee62fd734a9437ff5", - "version" : "1.3.2" + "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", + "version" : "1.4.2" } }, { @@ -185,8 +212,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { - "revision" : "a823735db57b46100b0c61cdfc5a08525b1e7cad", - "version" : "4.102.1" + "revision" : "a425e32f9b9d19c0ecab952cb4484c1c15e2536f", + "version" : "4.113.2" } }, { diff --git a/Package.swift b/Package.swift index e1f6ba2f..3cc707dc 100644 --- a/Package.swift +++ b/Package.swift @@ -21,11 +21,11 @@ let package = Package( dependencies: [ .package( url: "https://github.com/vapor/vapor.git", - .upToNextMajor(from: "4.102.1") + .upToNextMajor(from: "4.113.2") ), .package( url: "https://github.com/netreconlab/Parse-Swift.git", - .upToNextMajor(from: "5.11.2") + .upToNextMajor(from: "5.12.0") ) ], targets: [ diff --git a/Sources/ParseServerSwift/Parse.swift b/Sources/ParseServerSwift/Parse.swift index d7aac18e..6caf5854 100644 --- a/Sources/ParseServerSwift/Parse.swift +++ b/Sources/ParseServerSwift/Parse.swift @@ -71,6 +71,7 @@ func initializeServer( try await ParseSwift.initialize( applicationId: configuration.applicationId, primaryKey: configuration.primaryKey, + maintenanceKey: configuration.maintenanceKey, serverURL: parseServerURL, // POST all queries instead of using GET. usingPostForQuery: true, diff --git a/Sources/ParseServerSwift/ParseServerConfiguration.swift b/Sources/ParseServerSwift/ParseServerConfiguration.swift index 9b898e9d..16d72a72 100644 --- a/Sources/ParseServerSwift/ParseServerConfiguration.swift +++ b/Sources/ParseServerSwift/ParseServerConfiguration.swift @@ -16,6 +16,9 @@ public struct ParseServerConfiguration { /// The application id for your Node.js Parse Server application. public internal(set) var applicationId: String + /// The maintenance key for your Node.js Parse Server application. + public internal(set) var maintenanceKey: String? + /// The primary key for your Node.js Parse Server application. /// - note: This has been renamed from `masterKey` to reflect /// [inclusive language](https://github.com/dialpad/inclusive-language#motivation). @@ -43,17 +46,22 @@ public struct ParseServerConfiguration { - parameter tlsConfiguration: Manages configuration of TLS for SwiftNIO programs. - throws: An error of `ParseError` type. - important: This initializer looks for environment variables that begin - with **PARSE_SERVER_SWIFT** such as **PARSE_SERVER_SWIFT_APPLICATION_ID** - and **PARSE_SERVER_SWIFT_PRIMARY_KEY**. + with **PARSE_SERVER_SWIFT** such as **PARSE_SERVER_SWIFT_APPLICATION_ID**, + **PARSE_SERVER_SWIFT_MAINTENANCE_KEY**, and **PARSE_SERVER_SWIFT_PRIMARY_KEY**. */ - public init(app: Application, - tlsConfiguration: TLSConfiguration? = nil) throws { + public init( + app: Application, + tlsConfiguration: TLSConfiguration? = nil + ) throws { guard let applicationId = Environment.process.PARSE_SERVER_SWIFT_APPLICATION_ID, let primaryKey = Environment.process.PARSE_SERVER_SWIFT_PRIMARY_KEY else { - throw ParseError(code: .otherCause, - message: "Missing environment variables for applicationId or primaryKey") + throw ParseError( + code: .otherCause, + message: "Missing environment variables for applicationId or primaryKey" + ) } self.applicationId = applicationId + self.maintenanceKey = Environment.process.PARSE_SERVER_SWIFT_MAINTENANCE_KEY self.primaryKey = primaryKey app.http.server.configuration.hostname = Environment.process.PARSE_SERVER_SWIFT_HOST_NAME ?? "localhost" app.http.server.configuration.port = Int(Environment.process.PARSE_SERVER_SWIFT_PORT ?? 8080) @@ -88,16 +96,20 @@ public struct ParseServerConfiguration { needs to be one server. - throws: An error of `ParseError` type. */ - public init(app: Application, - hostName: String = "localhost", - port: Int = 8080, - tlsConfiguration: TLSConfiguration? = nil, - maxBodySize: ByteCount = "16kb", - applicationId: String, - primaryKey: String, - webhookKey: String? = nil, - parseServerURLString: String) throws { + public init( + app: Application, + hostName: String = "localhost", + port: Int = 8080, + tlsConfiguration: TLSConfiguration? = nil, + maxBodySize: ByteCount = "16kb", + applicationId: String, + maintenanceKey: String? = nil, + primaryKey: String, + webhookKey: String? = nil, + parseServerURLString: String + ) throws { self.applicationId = applicationId + self.maintenanceKey = maintenanceKey self.primaryKey = primaryKey self.webhookKey = webhookKey diff --git a/Tests/ParseServerSwiftTests/AppTests.swift b/Tests/ParseServerSwiftTests/AppTests.swift index 5de4fc19..4b3f1d9e 100644 --- a/Tests/ParseServerSwiftTests/AppTests.swift +++ b/Tests/ParseServerSwiftTests/AppTests.swift @@ -16,9 +16,10 @@ final class AppTests: XCTestCase { hostName: "hostName", port: 8080, applicationId: "applicationId", + maintenanceKey: "maintenanceKey", primaryKey: "primaryKey", webhookKey: hookKey, - parseServerURLString: "primaryKey" + parseServerURLString: "http://localhost:1337/1" ) try await ParseServerSwift.initialize( configuration, @@ -35,6 +36,7 @@ final class AppTests: XCTestCase { try await ParseSwift.initialize( applicationId: configuration.applicationId, primaryKey: configuration.primaryKey, + maintenanceKey: configuration.maintenanceKey, serverURL: parseServerURL, usingPostForQuery: true, requestCachePolicy: .reloadIgnoringLocalCacheData @@ -59,8 +61,9 @@ final class AppTests: XCTestCase { hostName: "hostName", port: 8080, applicationId: "applicationId", + maintenanceKey: "maintenanceKey", primaryKey: "primaryKey", - parseServerURLString: "primaryKey" + parseServerURLString: "http://localhost:1337/1" ) XCTAssertNoThrow(try setConfiguration(configuration)) try await app.asyncShutdown() @@ -73,8 +76,9 @@ final class AppTests: XCTestCase { hostName: "hostName", port: 8080, applicationId: "applicationId", + maintenanceKey: "maintenanceKey", primaryKey: "primaryKey", - parseServerURLString: "primaryKey" + parseServerURLString: "http://localhost:1337/1" ) XCTAssertThrowsError(try setConfiguration(configuration)) try await app.asyncShutdown() diff --git a/docker-compose.yml b/docker-compose.yml index 46d0e36b..d575b36a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ version: '3.7' x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} PARSE_SERVER_APPLICATION_ID: ${PARSE_SERVER_APPLICATION_ID} + PARSE_SERVER_MAINTENANCE_KEY: ${PARSE_SERVER_MAINTENANCE_KEY} PARSE_SERVER_PRIMARY_KEY: ${PARSE_SERVER_PRIMARY_KEY} PARSE_SERVER_READ_ONLY_PRIMARY_KEY: 367F7395-2E3A-46B1-ABA3-963A25D533C3 PARSE_SERVER_WEBHOOK_KEY: ${PARSE_SERVER_WEBHOOK_KEY} @@ -30,14 +31,17 @@ x-shared_environment: &shared_environment PARSE_SERVER_MOUNT_GRAPHQL: 'false' PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION: 'true' # Don't allow classes to be created on the client side. You can create classes by using ParseDashboard instead PARSE_SERVER_ALLOW_CUSTOM_OBJECTID: 'true' # Required to be true for ParseCareKit - PARSE_SERVER_ENABLE_SCHEMA_HOOKS: 'true' # When this is true, only need one server for PARSE_SERVER_SWIFT_URLS + PARSE_SERVER_ENABLE_SCHEMA_HOOKS: 'true' # When this is true, only need one server for PARSE_SERVER_SWIFT_URLS + PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION: 'true' + PARSE_SERVER_PAGES_ENABLE_ROUTER": 'true' PARSE_SERVER_DIRECT_ACCESS: 'false' # WARNING: Setting to 'true' is known to cause crashes on parse-hipaa running postgres PARSE_SERVER_ENABLE_PRIVATE_USERS: 'true' PARSE_SERVER_USING_PARSECAREKIT: 'false' # If you are not using ParseCareKit, set this to 'false' - PARSE_SERVER_RATE_LIMIT: 'true' + PARSE_SERVER_RATE_LIMIT: 'false' PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT: '100' PARSE_SERVER_RATE_LIMIT_INCLUDE_PRIMARY_KEY: 'false' PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS: 'false' + PARSE_SERVER_LIVEQUERY_CLASSNAMES: 'GameScore' PARSE_DASHBOARD_START: 'true' PARSE_DASHBOARD_APP_NAME: Parse HIPAA PARSE_DASHBOARD_USERNAMES: parse, parseRead @@ -47,6 +51,7 @@ x-shared_environment: &shared_environment PARSE_DASHBOARD_COOKIE_SESSION_SECRET: AB8849B6-D725-4A75-AA73-AB7103F0363F # This should be constant across all deployments on your system PARSE_DASHBOARD_MOUNT_PATH: /dashboard # This needs to be exactly what you plan it to be behind the proxy, i.e. If you want to access cs.uky.edu/dashboard it should be "/dashboard" PARSE_VERBOSE: 'false' + POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # Needed for wait-for-postgres.sh services: @@ -61,6 +66,7 @@ services: PARSE_SERVER_SWIFT_DEFAULT_MAX_BODY_SIZE: 16kb PARSE_SERVER_SWIFT_URLS: http://parse:1337/parse #,http://parse2:1337/parse # Only need to list one server. PARSE_SERVER_SWIFT_APPLICATION_ID: ${PARSE_SERVER_APPLICATION_ID} + PARSE_SERVER_SWIFT_MAINTENANCE_KEY: ${PARSE_SERVER_MAINTENANCE_KEY} PARSE_SERVER_SWIFT_PRIMARY_KEY: ${PARSE_SERVER_PRIMARY_KEY} PARSE_SERVER_SWIFT_WEBHOOK_KEY: ${PARSE_SERVER_WEBHOOK_KEY} # ports: @@ -74,9 +80,7 @@ services: depends_on: - parse parse: - image: netreconlab/parse-hipaa:6.4.0-dashboard - # Uncomment the image below to use Parse Server 5.4.0 instead. Be sure to comment out 6.0.0x - #image: netreconlab/parse-hipaa:5.4.0-dashboard + image: netreconlab/parse-hipaa:8.0.1-dashboard environment: <<: *shared_environment PARSE_SERVER_URL: http://parse:${PORT}${MOUNT_PATH} @@ -87,8 +91,6 @@ services: volumes: - ./parse/wait-for-postgres.sh:/parse-server/wait-for-postgres.sh - ./parse/index.js:/parse-server/index.js - # Uncomment the mount below to use Parse Server 5.4.0 instead. Be sure to comment out the index.js mount above - #- ./parse/index-5.4.0.js:/parse-server/index.js - ./parse/cloud:/parse-server/cloud - ./parse/files:/parse-server/files # All files uploaded from users are stored to an ecrypted drive locally for HIPAA compliance restart: always diff --git a/parse/index-6.x.x.js b/parse/index-6.x.x.js new file mode 100644 index 00000000..7e0e2ece --- /dev/null +++ b/parse/index-6.x.x.js @@ -0,0 +1,662 @@ +// Example express application adding the parse-server module to expose Parse +// compatible API routes. + +const { default: ParseServer, RedisCacheAdapter } = require('./lib'); +const { GridFSBucketAdapter } = require('./lib/Adapters/Files/GridFSBucketAdapter'); +const ParseAuditor = require('./node_modules/parse-auditor/src/index.js'); +const express = require('express'); +const path = require('path'); +const cors = require('cors'); +const FSFilesAdapter = require('@parse/fs-files-adapter'); + +const mountPath = process.env.PARSE_SERVER_MOUNT_PATH || '/parse'; +const graphMountPath = process.env.PARSE_SERVER_GRAPHQL_PATH || '/graphql'; +const dashboardMountPath = process.env.PARSE_DASHBOARD_MOUNT_PATH || '/dashboard'; +const applicationId = process.env.PARSE_SERVER_APPLICATION_ID || 'myAppId'; +const primaryKey = process.env.PARSE_SERVER_PRIMARY_KEY || 'myKey'; +const redisURL = process.env.PARSE_SERVER_REDIS_URL || process.env.REDIS_TLS_URL || process.env.REDIS_URL; +const host = process.env.HOST || process.env.PARSE_SERVER_HOST || '0.0.0.0'; +const port = process.env.PORT || 1337; +let serverURL = process.env.PARSE_SERVER_URL || 'http://localhost:' + process.env.PORT + mountPath; +let appName = 'myApp'; +if ("NEW_RELIC_APP_NAME" in process.env) { + require ('newrelic'); + appName = process.env.NEW_RELIC_APP_NAME; + if (!("PARSE_SERVER_URL" in process.env)) { + serverURL = `https://${appName}.herokuapp.com${mountPath}`; + } +} + +const publicServerURL = process.env.PARSE_SERVER_PUBLIC_URL || serverURL; +const url = new URL(publicServerURL); +const graphURL = new URL(publicServerURL); +graphURL.pathname = graphMountPath; +const dashboardURL = new URL(publicServerURL); +dashboardURL.pathname = dashboardMountPath; + +let enableParseServer = true; +if (process.env.PARSE_SERVER_ENABLE == 'false') { + enableParseServer = false +} + +let startLiveQueryServer = true; +if (process.env.PARSE_SERVER_START_LIVE_QUERY_SERVER == 'false') { + startLiveQueryServer = false +} + +let startLiveQueryServerNoParse = false; +if (process.env.PARSE_SERVER_START_LIVE_QUERY_SERVER_NO_PARSE == 'true') { + startLiveQueryServerNoParse = true +} + +let enableDashboard = false; +if (process.env.PARSE_DASHBOARD_START == 'true') { + enableDashboard = true +} + +let verbose = false; +if (process.env.PARSE_VERBOSE == 'true') { + verbose = true +} + +let configuration; +const logsFolder = process.env.PARSE_SERVER_LOGS_FOLDER || './logs'; +const fileMaxUploadSize = process.env.PARSE_SERVER_MAX_UPLOAD_SIZE || '20mb'; +const cacheMaxSize = parseInt(process.env.PARSE_SERVER_CACHE_MAX_SIZE) || 10000; +const cacheTTL = parseInt(process.env.PARSE_SERVER_CACHE_TTL) || 5000; +const objectIdSize = parseInt(process.env.PARSE_SERVER_OBJECT_ID_SIZE) || 10; +const sessionLength = parseInt(process.env.PARSE_SERVER_SESSION_LENGTH) || 31536000; +const emailVerifyTokenValidityDuration = parseInt(process.env.PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION) || 24*60*60; +const accountLockoutDuration = parseInt(process.env.PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION) || 5; +const accountLockoutThreshold = parseInt(process.env.PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD) || 5; +const maxPasswordHistory = parseInt(process.env.PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY) || 5; +const resetTokenValidityDuration = parseInt(process.env.PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION) || 24*60*60; +const validationError = process.env.PARSE_SERVER_PASSWORD_POLICY_VALIDATION_ERROR || 'Password must have at least 8 characters, contain at least 1 digit, 1 lower case, 1 upper case, and contain at least one special character.'; +const validatorPattern = process.env.PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN || /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/; +const triggerAfter = process.env.PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER || 'info'; +const triggerBeforeError = process.env.PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR || 'error'; +const triggerBeforeSuccess = process.env.PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS || 'info'; +const playgroundPath = process.env.PARSE_SERVER_MOUNT_PLAYGROUND || '/playground'; +const websocketTimeout = process.env.PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT || 10 * 1000; +const cacheTimeout = process.env.PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT || 5 * 1000; +const logLevel = process.env.PARSE_LIVE_QUERY_SERVER_LOG_LEVEL || 'INFO'; +let primaryKeyIPs = process.env.PARSE_SERVER_PRIMARY_KEY_IPS || '172.16.0.0/12, 192.168.0.0/16, 10.0.0.0/8, 127.0.0.1, ::1'; +primaryKeyIPs = primaryKeyIPs.split(", "); +let classNames = process.env.PARSE_SERVER_LIVEQUERY_CLASSNAMES || 'Clock'; +classNames = classNames.split(", "); +let trustServerProxy = process.env.PARSE_SERVER_TRUST_PROXY || false; +if (trustServerProxy == 'true') { + trustServerProxy = true; +} + +let enableGraphQL = false; +if (process.env.PARSE_SERVER_MOUNT_GRAPHQL == 'true') { + enableGraphQL = true +} + +let allowNewClasses = false; +if (process.env.PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION == 'true') { + allowNewClasses = true +} + +let allowCustomObjectId = false; +if (process.env.PARSE_SERVER_ALLOW_CUSTOM_OBJECTID == 'true') { + allowCustomObjectId = true +} + +let enableSchemaHooks = false; +if (process.env.PARSE_SERVER_DATABASE_ENABLE_SCHEMA_HOOKS == 'true') { + enableSchemaHooks = true +} + +let useDirectAccess = false; +if (process.env.PARSE_SERVER_DIRECT_ACCESS == 'true') { + useDirectAccess = true +} + +let enforcePrivateUsers = false; +if (process.env.PARSE_SERVER_ENFORCE_PRIVATE_USERS == 'true') { + enforcePrivateUsers = true +} + +let fileUploadPublic = false; +if (process.env.PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC == 'true') { + fileUploadPublic = true +} + +let fileUploadAnonymous = true; +if (process.env.PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER == 'false') { + fileUploadAnonymous = false +} + +let fileUploadAuthenticated = true; +if (process.env.PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER == 'false') { + fileUploadAuthenticated = false +} + +let enableAnonymousUsers = true; +if (process.env.PARSE_SERVER_ENABLE_ANON_USERS == 'false') { + enableAnonymousUsers = false +} + +let enableIdempotency = false; +if (process.env.PARSE_SERVER_ENABLE_IDEMPOTENCY == 'true') { + enableIdempotency = true +} + +let allowExpiredAuthDataToken = false; +if (process.env.PARSE_SERVER_ALLOW_EXPIRED_AUTH_DATA_TOKEN == 'true') { + allowExpiredAuthDataToken = true +} + +let emailVerifyTokenReuseIfValid = false; +if (process.env.PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID == 'true') { + emailVerifyTokenReuseIfValid = true +} + +let expireInactiveSessions = true; +if (process.env.PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS == 'false') { + expireInactiveSessions = false +} + +let jsonLogs = false; +if (process.env.JSON_LOGS == 'true') { + jsonLogs = true +} + +let preserveFileName = false; +if (process.env.PARSE_SERVER_PRESERVE_FILE_NAME == 'true') { + preserveFileName = true +} + +let revokeSessionOnPasswordReset = true; +if (process.env.PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET == 'false') { + revokeSessionOnPasswordReset = false +} + +let verifyUserEmails = false; +if (process.env.PARSE_SERVER_VERIFY_USER_EMAILS == 'true') { + verifyUserEmails = true +} + +let unlockOnPasswordReset = false; +if (process.env.PARSE_SERVER_ACCOUNT_LOCKOUT_UNLOCK_ON_PASSWORD_RESET == 'true') { + unlockOnPasswordReset = true +} + +let doNotAllowUsername = false; +if (process.env.PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME == 'true') { + doNotAllowUsername = true +} + +let resetTokenReuseIfValid = false; +if (process.env.PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID == 'true') { + resetTokenReuseIfValid = true +} + +let preventLoginWithUnverifiedEmail = false; +if (process.env.PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL == 'true') { + preventLoginWithUnverifiedEmail = true +} + +let mountPlayground = false; +if (process.env.PARSE_SERVER_MOUNT_PLAYGROUND == 'true') { + mountPlayground = true; +} + +let pushNotifications = process.env.PARSE_SERVER_PUSH || {}; +let authentication = process.env.PARSE_SERVER_AUTH_PROVIDERS || {}; + +let databaseUri = process.env.PARSE_SERVER_DATABASE_URI || process.env.DB_URL; +if (!databaseUri) { + console.log('PARSE_SERVER_DATABASE_URI or DB_URL not specified, falling back to localhost.'); +} + +// Need to use local file adapter for postgres +let filesAdapter = {}; +let filesFSAdapterOptions = {} +if ("PARSE_SERVER_ENCRYPTION_KEY" in process.env) { + filesFSAdapterOptions.encryptionKey = process.env.PARSE_SERVER_ENCRYPTION_KEY; +} + +if ("PARSE_SERVER_S3_BUCKET" in process.env) { + filesAdapter = { + "module": "@parse/s3-files-adapter", + "options": { + "bucket": process.env.PARSE_SERVER_S3_BUCKET, + "region": process.env.PARSE_SERVER_S3_BUCKET_REGION || 'us-east-2', + "ServerSideEncryption": process.env.PARSE_SERVER_S3_BUCKET_ENCRYPTION || 'AES256', //AES256 or aws:kms, or if you do not pass this, encryption won't be done + } + } +} else if ("PARSE_SERVER_DATABASE_URI" in process.env) { + if (process.env.PARSE_SERVER_DATABASE_URI.indexOf('postgres') !== -1) { + filesAdapter = new FSFilesAdapter(filesFSAdapterOptions); + } +} else if ("DB_URL" in process.env) { + if (process.env.DB_URL.indexOf('postgres') !== -1) { + filesAdapter = new FSFilesAdapter(filesFSAdapterOptions); + databaseUri = `${databaseUri}?ssl=true&rejectUnauthorized=false`; + } +} + +if (Object.keys(filesAdapter).length === 0) { + filesAdapter = new GridFSBucketAdapter( + databaseUri, + {}, + process.env.PARSE_SERVER_ENCRYPTION_KEY + ); +} + +configuration = { + databaseURI: databaseUri || 'mongodb://localhost:27017/dev', + cloud: process.env.PARSE_SERVER_CLOUD || __dirname + '/cloud/main.js', + appId: applicationId, + masterKey: primaryKey, + masterKeyIps: primaryKeyIPs, + webhookKey: process.env.PARSE_SERVER_WEBHOOK_KEY, + encryptionKey: process.env.PARSE_SERVER_ENCRYPTION_KEY, + objectIdSize: objectIdSize, + serverURL: serverURL, + publicServerURL: publicServerURL, + host: host, + port: port, + trustProxy: trustServerProxy, + cacheMaxSize: cacheMaxSize, + cacheTTL: cacheTTL, + verbose: verbose, + allowClientClassCreation: allowNewClasses, + allowCustomObjectId: allowCustomObjectId, + enableAnonymousUsers: enableAnonymousUsers, + emailVerifyTokenReuseIfValid: emailVerifyTokenReuseIfValid, + expireInactiveSessions: expireInactiveSessions, + filesAdapter: filesAdapter, + fileUpload: { + enableForPublic: fileUploadPublic, + enableForAnonymousUser: fileUploadAnonymous, + enableForAuthenticatedUser: fileUploadAuthenticated, + }, + maxUploadSize: fileMaxUploadSize, + enableSchemaHooks: enableSchemaHooks, + directAccess: useDirectAccess, + allowExpiredAuthDataToken: allowExpiredAuthDataToken, + enforcePrivateUsers: enforcePrivateUsers, + jsonLogs: jsonLogs, + logsFolder: logsFolder, + preserveFileName: preserveFileName, + revokeSessionOnPasswordReset: revokeSessionOnPasswordReset, + sessionLength: sessionLength, + // Setup your push adatper + push: pushNotifications, + auth: authentication, + startLiveQueryServer: startLiveQueryServer, + liveQuery: { + classNames: classNames, // List of classes to support for query subscriptions + websocketTimeout: websocketTimeout, + cacheTimeout: cacheTimeout + }, + mountGraphQL: enableGraphQL, + graphMountPath: graphMountPath, + mountPlayground: mountPlayground, + playgroundPath: playgroundPath, + verifyUserEmails: verifyUserEmails, + // Setup your mail adapter + /*emailAdapter: { + module: 'parse-server-api-mail-adapter', + /*options: { + // The address that your emails come from + sender: '', + templates: { + passwordResetEmail: { + subject: 'Reset your password', + pathPlainText: path.join(__dirname, 'email-templates/password_reset_email.txt'), + pathHtml: path.join(__dirname, 'email-templates/password_reset_email.html'), + callback: (user) => {}//{ return { firstName: user.get('firstName') }} + // Now you can use {{firstName}} in your templates + }, + verificationEmail: { + subject: 'Confirm your account', + pathPlainText: path.join(__dirname, 'email-templates/verification_email.txt'), + pathHtml: path.join(__dirname, 'email-templates/verification_email.html'), + callback: (user) => {}//{ return { firstName: user.get('firstName') }} + // Now you can use {{firstName}} in your templates + }, + customEmailAlert: { + subject: 'Urgent notification!', + pathPlainText: path.join(__dirname, 'email-templates/custom_email.txt'), + pathHtml: path.join(__dirname, 'email-templates/custom_email.html'), + } + } + } + },*/ + emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration, // in seconds (2 hours = 7200 seconds) + // set preventLoginWithUnverifiedEmail to false to allow user to login without verifying their email + // set preventLoginWithUnverifiedEmail to true to prevent user from login if their email is not verified + preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail, // defaults to false + // account lockout policy setting (OPTIONAL) - defaults to undefined + // if the account lockout policy is set and there are more than `threshold` number of failed login attempts then the `login` api call returns error code `Parse.Error.OBJECT_NOT_FOUND` with error message `Your account is locked due to multiple failed login attempts. Please try again after minute(s)`. After `duration` minutes of no login attempts, the application will allow the user to try login again. + accountLockout: { + duration: accountLockoutDuration, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000. + threshold: accountLockoutThreshold, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000. + unlockOnPasswordReset: unlockOnPasswordReset, + }, + // optional settings to enforce password policies + passwordPolicy: { + // Two optional settings to enforce strong passwords. Either one or both can be specified. + // If both are specified, both checks must pass to accept the password + // 1. a RegExp object or a regex string representing the pattern to enforce + validatorPattern: validatorPattern, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit + // 2. a callback function to be invoked to validate the password + //validatorCallback: (password) => { return validatePassword(password) }, + validationError: validationError, // optional error message to be sent instead of the default "Password does not meet the Password Policy requirements." message. + doNotAllowUsername: doNotAllowUsername, // optional setting to disallow username in passwords + maxPasswordHistory: maxPasswordHistory, // optional setting to prevent reuse of previous n passwords. Maximum value that can be specified is 20. Not specifying it or specifying 0 will not enforce history. + //optional setting to set a validity duration for password reset links (in seconds) + resetTokenReuseIfValid: resetTokenReuseIfValid, + resetTokenValidityDuration: resetTokenValidityDuration, // expire after 24 hours + }, + logLevels: { + triggerAfter: triggerAfter, + triggerBeforeError: triggerBeforeError, + triggerBeforeSuccess: triggerBeforeSuccess, + } +}; + +if ("PARSE_SERVER_READ_ONLY_PRIMARY_KEY" in process.env) { + configuration.readOnlyMasterKey = process.env.PARSE_SERVER_READ_ONLY_PRIMARY_KEY; +} + +if (("PARSE_SERVER_REDIS_URL" in process.env) || ("REDIS_TLS_URL" in process.env) || ("REDIS_URL" in process.env)) { + const redisOptions = { url: redisURL }; + configuration.cacheAdapter = new RedisCacheAdapter(redisOptions); + // Set LiveQuery URL + configuration.liveQuery.redisURL = redisURL; +} + +// Rate limiting +let rateLimit = false; +if (process.env.PARSE_SERVER_RATE_LIMIT == 'true') { + rateLimit = true; +} + +if (rateLimit == true) { + configuration.rateLimit = []; + const firstRateLimit = {}; + firstRateLimit.requestPath = process.env.PARSE_SERVER_RATE_LIMIT_REQUEST_PATH || '*'; + firstRateLimit.errorResponseMessage = process.env.PARSE_SERVER_RATE_LIMIT_ERROR_RESPONSE_MESSAGE || 'Too many requests'; + firstRateLimit.requestCount = parseInt(process.env.PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT) || 100; + firstRateLimit.requestTimeWindow = parseInt(process.env.PARSE_SERVER_RATE_LIMIT_REQUEST_TIME_WINDOW) || 10 * 60 * 1000; + if (process.env.PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS == 'true') { + firstRateLimit.includeInternalRequests = true; + } + if (process.env.PARSE_SERVER_RATE_LIMIT_INCLUDE_PRIMARY_KEY == 'true') { + firstRateLimit.includeMasterKey = true; + } + if ("PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS" in process.env) { + const requestMethods = process.env.PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS.split(", "); + firstRateLimit.requestMethods = requestMethods; + } + configuration.rateLimit.push(firstRateLimit); +} + +if ("PARSE_SERVER_GRAPH_QLSCHEMA" in process.env) { + configuration.graphQLSchema = process.env.PARSE_SERVER_GRAPH_QLSCHEMA; +} + +if ("PARSE_SERVER_ALLOW_HEADERS" in process.env) { + configuration.allowHeaders = process.env.PARSE_SERVER_ALLOW_HEADERS; +} + +if ("PARSE_SERVER_ALLOW_ORIGIN" in process.env) { + configuration.allowOrigin = process.env.PARSE_SERVER_ALLOW_ORIGIN; +} + +if ("PARSE_SERVER_MAX_LIMIT" in process.env) { + configuration.maxLimit = parseInt(process.env.PARSE_SERVER_MAX_LIMIT); +} + +if ("PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE" in process.env) { + configuration.passwordPolicy.maxPasswordAge = parseInt(process.env.PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE); +} + +if (enableIdempotency) { + let paths = process.env.PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS || '.*'; + paths = paths.split(", "); + const ttl = process.env.PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL || 300; + configuration.idempotencyOptions = { + paths: paths, + ttl: ttl + }; +} + +let app = express(); + +// Enable All CORS Requests +app.use(cors()); + +// Redirect to https if on Heroku +app.use(function(request, response, next) { + if (("NEW_RELIC_APP_NAME" in process.env) && !request.secure) + return response.redirect("https://" + request.headers.host + request.url); + next(); +}); + +async function setupParseServer() { + const parseServer = await ParseServer.startApp(configuration); + app = parseServer.expressApp; + + // Enable All CORS Requests + app.use(cors()); + + // Serve static assets from the /public folder + app.use('/public', express.static(path.join(__dirname, '/public'))); + + // Parse Server plays nicely with the rest of your web routes + app.get('/', function(_req, res) { + res.status(200).send('I dream of being a website. Please star the parse-hipaa repo on GitHub!'); + }); + + // Redirect to https if on Heroku + app.use(function(request, response, next) { + if (("NEW_RELIC_APP_NAME" in process.env) && !request.secure) + return response.redirect("https://" + request.headers.host + request.url); + next(); + }); + + setupDashboard(); + + console.log('Public access: ' + url.hostname + ', Local access: ' + serverURL); + console.log(`REST API running on ${url.href}`); + if (startLiveQueryServer) + console.log(`LiveQuery server is now available at ${url.href}`); + if (enableGraphQL) + console.log(`GraphQL API running on ${graphURL.href}`); + if (enableDashboard) + console.log(`Dashboard is now available at ${dashboardURL.href}`); + + if (process.env.PARSE_SERVER_USING_PARSECAREKIT == 'true') { + const { CareKitServer } = require('parse-server-carekit'); + let shouldAudit = true; + if (process.env.PARSE_SERVER_USING_PARSECAREKIT_AUDIT === 'false') { + shouldAudit = false; + } + if (shouldAudit) { + setAuditClassLevelPermissions(); + } + let careKitServer = new CareKitServer(parseServer, shouldAudit); + await careKitServer.setup(); + } +} + +function setAuditClassLevelPermissions() { + const auditCLP = { + get: { requiresAuthentication: true }, + find: { requiresAuthentication: true }, + create: { }, + update: { requiresAuthentication: true }, + delete: { requiresAuthentication: true }, + addField: { }, + protectedFields: { } + }; + // Don't audit '_Role' as it doesn't work. + const modifiedClasses = ['_User', '_Installation', '_Audience', 'Clock', 'Patient', 'CarePlan', 'Contact', 'Task', 'HealthKitTask', 'Outcome', 'HealthKitOutcome']; + const accessedClasses = ['_User', '_Installation', '_Audience', 'Clock', 'Patient', 'CarePlan', 'Contact', 'Task', 'HealthKitTask', 'Outcome', 'HealthKitOutcome']; + ParseAuditor(modifiedClasses, accessedClasses, { classPostfix: '_Audit', useMasterKey: true, clp: auditCLP }); +}; + +function setupDashboard() { + if (enableDashboard) { + const fs = require('fs'); + const ParseDashboard = require('parse-dashboard'); + + const allowInsecureHTTP = process.env.PARSE_DASHBOARD_ALLOW_INSECURE_HTTP; + const cookieSessionSecret = process.env.PARSE_DASHBOARD_COOKIE_SESSION_SECRET; + const trustProxy = process.env.PARSE_DASHBOARD_TRUST_PROXY; + + if (trustProxy && allowInsecureHTTP) { + console.log('Set only trustProxy *or* allowInsecureHTTP, not both. Only one is needed to handle being behind a proxy.'); + process.exit(1); + } + + let configFile = null; + let configFromCLI = null; + const configServerURL = process.env.PARSE_DASHBOARD_SERVER_URL || serverURL; + const configGraphQLServerURL = process.env.PARSE_DASHBOARD_GRAPHQL_SERVER_URL || graphURL.href; + const configPrimaryKey = process.env.PARSE_DASHBOARD_PRIMARY_KEY || primaryKey; + const configAppId = process.env.PARSE_DASHBOARD_APP_ID || applicationId; + const configAppName = process.env.PARSE_DASHBOARD_APP_NAME || appName; + let configUsernames = process.env.PARSE_DASHBOARD_USERNAMES; + let configUserPasswords = process.env.PARSE_DASHBOARD_USER_PASSWORDS; + let configUserPasswordEncrypted = true; + if (process.env.PARSE_DASHBOARD_USER_PASSWORD_ENCRYPTED == 'false') { + configUserPasswordEncrypted = false; + } + + if (!process.env.PARSE_DASHBOARD_CONFIG) { + if (configServerURL && configPrimaryKey && configAppId) { + configFromCLI = { + data: { + apps: [ + { + appId: configAppId, + serverURL: configServerURL, + masterKey: configPrimaryKey, + appName: configAppName, + }, + ] + } + }; + if (configGraphQLServerURL) { + configFromCLI.data.apps[0].graphQLServerURL = configGraphQLServerURL; + } + if (configUsernames && configUserPasswords) { + configUsernames = configUsernames.split(", "); + configUserPasswords = configUserPasswords.split(", "); + if (configUsernames.length == configUserPasswords.length) { + let users = []; + configUsernames.forEach((username, index) => { + users.push({ + user: username, + pass: configUserPasswords[index], + }); + }); + configFromCLI.data.users = users; + configFromCLI.data.useEncryptedPasswords = configUserPasswordEncrypted; + } else { + console.log('Dashboard usernames(' + configUsernames.length + ') ' + 'and passwords(' + configUserPasswords.length + ') must be the same size.'); + process.exit(1); + } + } + } else if (!configServerURL && !configPrimaryKey && !configAppName) { + configFile = path.join(__dirname, 'parse-dashboard-config.json'); + } + } else { + configFromCLI = { + data: JSON.parse(process.env.PARSE_DASHBOARD_CONFIG) + }; + } + + let config = null; + let configFilePath = null; + if (configFile) { + try { + config = { + data: JSON.parse(fs.readFileSync(configFile, 'utf8')) + }; + configFilePath = path.dirname(configFile); + } catch (error) { + if (error instanceof SyntaxError) { + console.log('Your config file contains invalid JSON. Exiting.'); + process.exit(1); + } else if (error.code === 'ENOENT') { + console.log('You must provide either a config file or required CLI options (app ID, Primary Key, and server URL); not both.'); + process.exit(3); + } else { + console.log('There was a problem with your config. Exiting.'); + process.exit(1); + } + } + } else if (configFromCLI) { + config = configFromCLI; + } else { + //Failed to load default config file. + console.log('You must provide either a config file or an app ID, Primary Key, and server URL. See parse-dashboard --help for details.'); + process.exit(4); + } + + config.data.apps.forEach(app => { + if (!app.appName) { + app.appName = app.appId; + } + }); + + if (config.data.iconsFolder && configFilePath) { + config.data.iconsFolder = path.join(configFilePath, config.data.iconsFolder); + } + + if (enableParseServer == false) { + if (allowInsecureHTTP || trustProxy) app.enable('trust proxy', trustProxy); + config.data.trustProxy = trustProxy; + } else { + config.data.trustProxy = configuration.trustProxy; + } + + const dashboardOptions = { allowInsecureHTTP, cookieSessionSecret }; + const dashboard = new ParseDashboard(config.data, dashboardOptions); + app.use(dashboardMountPath, dashboard); + } +} + +if (enableParseServer) { + setupParseServer(); +} else { + setupDashboard(); + const httpServer = require('http').createServer(app); + + if (startLiveQueryServerNoParse == true) { + let liveQueryConfig = { + appId: applicationId, + masterKey: primaryKey, + serverURL: serverURL, + websocketTimeout: websocketTimeout, + cacheTimeout: cacheTimeout, + verbose: verbose, + logLevel: logLevel, + } + + if (("PARSE_SERVER_REDIS_URL" in process.env) || ("REDIS_TLS_URL" in process.env) || ("REDIS_URL" in process.env)) { + liveQueryConfig.redisURL = redisURL; + } + + // This will enable the Live Query real-time server + ParseServer.createLiveQueryServer(httpServer, liveQueryConfig, configuration); + } + + httpServer.listen(port, host, function() { + + if (startLiveQueryServerNoParse) + console.log(`LiveQuery server is now available at ${url.href}`); + + if (enableDashboard) + console.log(`Dashboard is now available at ${dashboardURL.href}`); + }); +} \ No newline at end of file diff --git a/parse/index.js b/parse/index.js index 7e0e2ece..13828505 100644 --- a/parse/index.js +++ b/parse/index.js @@ -10,9 +10,10 @@ const cors = require('cors'); const FSFilesAdapter = require('@parse/fs-files-adapter'); const mountPath = process.env.PARSE_SERVER_MOUNT_PATH || '/parse'; -const graphMountPath = process.env.PARSE_SERVER_GRAPHQL_PATH || '/graphql'; +const graphQLPath = process.env.PARSE_SERVER_GRAPHQL_PATH || '/graphql'; const dashboardMountPath = process.env.PARSE_DASHBOARD_MOUNT_PATH || '/dashboard'; const applicationId = process.env.PARSE_SERVER_APPLICATION_ID || 'myAppId'; +const maintenanceKey = process.env.PARSE_SERVER_MAINTENANCE_KEY || 'myMaintenanceKey'; const primaryKey = process.env.PARSE_SERVER_PRIMARY_KEY || 'myKey'; const redisURL = process.env.PARSE_SERVER_REDIS_URL || process.env.REDIS_TLS_URL || process.env.REDIS_URL; const host = process.env.HOST || process.env.PARSE_SERVER_HOST || '0.0.0.0'; @@ -30,7 +31,7 @@ if ("NEW_RELIC_APP_NAME" in process.env) { const publicServerURL = process.env.PARSE_SERVER_PUBLIC_URL || serverURL; const url = new URL(publicServerURL); const graphURL = new URL(publicServerURL); -graphURL.pathname = graphMountPath; +graphURL.pathname = graphQLPath; const dashboardURL = new URL(publicServerURL); dashboardURL.pathname = dashboardMountPath; @@ -80,9 +81,11 @@ const playgroundPath = process.env.PARSE_SERVER_MOUNT_PLAYGROUND || '/playground const websocketTimeout = process.env.PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT || 10 * 1000; const cacheTimeout = process.env.PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT || 5 * 1000; const logLevel = process.env.PARSE_LIVE_QUERY_SERVER_LOG_LEVEL || 'INFO'; -let primaryKeyIPs = process.env.PARSE_SERVER_PRIMARY_KEY_IPS || '172.16.0.0/12, 192.168.0.0/16, 10.0.0.0/8, 127.0.0.1, ::1'; -primaryKeyIPs = primaryKeyIPs.split(", "); -let classNames = process.env.PARSE_SERVER_LIVEQUERY_CLASSNAMES || 'Clock'; +let maintenanceKeyIps = process.env.PARSE_SERVER_MAINTENANCE_KEY_IPS || '172.16.0.0/12, 192.168.0.0/16, 10.0.0.0/8, 127.0.0.1, ::1'; +maintenanceKeyIps = maintenanceKeyIps.split(", "); +let primaryKeyIps = process.env.PARSE_SERVER_PRIMARY_KEY_IPS || '172.16.0.0/12, 192.168.0.0/16, 10.0.0.0/8, 127.0.0.1, ::1'; +primaryKeyIps = primaryKeyIps.split(", "); +let classNames = process.env.PARSE_SERVER_LIVEQUERY_CLASSNAMES || 'GameScore'; classNames = classNames.split(", "); let trustServerProxy = process.env.PARSE_SERVER_TRUST_PROXY || false; if (trustServerProxy == 'true') { @@ -109,6 +112,20 @@ if (process.env.PARSE_SERVER_DATABASE_ENABLE_SCHEMA_HOOKS == 'true') { enableSchemaHooks = true } +let encodeParseObjectInCloudFunction = false; +if (process.env.PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION == 'true') { + encodeParseObjectInCloudFunction = true +} + +let enablePagesRouter = false; +if (process.env.PARSE_SERVER_PAGES_ENABLE_ROUTER == 'true') { + enablePagesRouter = true +} + +const pagesOptions = { + enableRouter: enablePagesRouter, +}; + let useDirectAccess = false; if (process.env.PARSE_SERVER_DIRECT_ACCESS == 'true') { useDirectAccess = true @@ -134,6 +151,12 @@ if (process.env.PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER == 'false fileUploadAuthenticated = false } +let fileExtensions = ['^[^hH][^tT][^mM][^lL]?$']; +if ("PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS" in process.env) { + const extensions = process.env.PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS.split(", "); + fileExtensions = extensions; +} + let enableAnonymousUsers = true; if (process.env.PARSE_SERVER_ENABLE_ANON_USERS == 'false') { enableAnonymousUsers = false @@ -219,6 +242,17 @@ if ("PARSE_SERVER_ENCRYPTION_KEY" in process.env) { filesFSAdapterOptions.encryptionKey = process.env.PARSE_SERVER_ENCRYPTION_KEY; } +if ("PARSE_SERVER_DATABASE_URI" in process.env) { + if (process.env.PARSE_SERVER_DATABASE_URI.indexOf('postgres') !== -1) { + filesAdapter = new FSFilesAdapter(filesFSAdapterOptions); + } +} else if ("DB_URL" in process.env) { + if (process.env.DB_URL.indexOf('postgres') !== -1) { + filesAdapter = new FSFilesAdapter(filesFSAdapterOptions); + databaseUri = `${databaseUri}?ssl=true&rejectUnauthorized=false`; + } +} + if ("PARSE_SERVER_S3_BUCKET" in process.env) { filesAdapter = { "module": "@parse/s3-files-adapter", @@ -228,15 +262,6 @@ if ("PARSE_SERVER_S3_BUCKET" in process.env) { "ServerSideEncryption": process.env.PARSE_SERVER_S3_BUCKET_ENCRYPTION || 'AES256', //AES256 or aws:kms, or if you do not pass this, encryption won't be done } } -} else if ("PARSE_SERVER_DATABASE_URI" in process.env) { - if (process.env.PARSE_SERVER_DATABASE_URI.indexOf('postgres') !== -1) { - filesAdapter = new FSFilesAdapter(filesFSAdapterOptions); - } -} else if ("DB_URL" in process.env) { - if (process.env.DB_URL.indexOf('postgres') !== -1) { - filesAdapter = new FSFilesAdapter(filesFSAdapterOptions); - databaseUri = `${databaseUri}?ssl=true&rejectUnauthorized=false`; - } } if (Object.keys(filesAdapter).length === 0) { @@ -249,10 +274,15 @@ if (Object.keys(filesAdapter).length === 0) { configuration = { databaseURI: databaseUri || 'mongodb://localhost:27017/dev', + databaseOptions: { + enableSchemaHooks: enableSchemaHooks, + }, cloud: process.env.PARSE_SERVER_CLOUD || __dirname + '/cloud/main.js', appId: applicationId, + maintenanceKey: maintenanceKey, + maintenanceKeyIps: maintenanceKeyIps, masterKey: primaryKey, - masterKeyIps: primaryKeyIPs, + masterKeyIps: primaryKeyIps, webhookKey: process.env.PARSE_SERVER_WEBHOOK_KEY, encryptionKey: process.env.PARSE_SERVER_ENCRYPTION_KEY, objectIdSize: objectIdSize, @@ -274,14 +304,16 @@ configuration = { enableForPublic: fileUploadPublic, enableForAnonymousUser: fileUploadAnonymous, enableForAuthenticatedUser: fileUploadAuthenticated, + fileExtensions: fileExtensions, }, maxUploadSize: fileMaxUploadSize, - enableSchemaHooks: enableSchemaHooks, + encodeParseObjectInCloudFunction: encodeParseObjectInCloudFunction, directAccess: useDirectAccess, allowExpiredAuthDataToken: allowExpiredAuthDataToken, enforcePrivateUsers: enforcePrivateUsers, jsonLogs: jsonLogs, logsFolder: logsFolder, + pages: pagesOptions, preserveFileName: preserveFileName, revokeSessionOnPasswordReset: revokeSessionOnPasswordReset, sessionLength: sessionLength, @@ -291,11 +323,9 @@ configuration = { startLiveQueryServer: startLiveQueryServer, liveQuery: { classNames: classNames, // List of classes to support for query subscriptions - websocketTimeout: websocketTimeout, - cacheTimeout: cacheTimeout }, mountGraphQL: enableGraphQL, - graphMountPath: graphMountPath, + graphQLPath: graphQLPath, mountPlayground: mountPlayground, playgroundPath: playgroundPath, verifyUserEmails: verifyUserEmails, @@ -498,8 +528,8 @@ function setAuditClassLevelPermissions() { protectedFields: { } }; // Don't audit '_Role' as it doesn't work. - const modifiedClasses = ['_User', '_Installation', '_Audience', 'Clock', 'Patient', 'CarePlan', 'Contact', 'Task', 'HealthKitTask', 'Outcome', 'HealthKitOutcome']; - const accessedClasses = ['_User', '_Installation', '_Audience', 'Clock', 'Patient', 'CarePlan', 'Contact', 'Task', 'HealthKitTask', 'Outcome', 'HealthKitOutcome']; + const modifiedClasses = ['_User', '_Installation', '_Audience', 'Clock', 'Patient', 'CarePlan', 'Contact', 'Task', 'HealthKitTask', 'Outcome', 'HealthKitOutcome', 'RevisionRecord']; + const accessedClasses = ['_User', '_Installation', '_Audience', 'Clock', 'Patient', 'CarePlan', 'Contact', 'Task', 'HealthKitTask', 'Outcome', 'HealthKitOutcome', 'RevisionRecord']; ParseAuditor(modifiedClasses, accessedClasses, { classPostfix: '_Audit', useMasterKey: true, clp: auditCLP }); }; @@ -659,4 +689,4 @@ if (enableParseServer) { if (enableDashboard) console.log(`Dashboard is now available at ${dashboardURL.href}`); }); -} \ No newline at end of file +} diff --git a/parse/wait-for-postgres.sh b/parse/wait-for-postgres.sh index fe2e6f4d..71728ef2 100755 --- a/parse/wait-for-postgres.sh +++ b/parse/wait-for-postgres.sh @@ -1,16 +1,26 @@ #!/bin/sh # wait-for-postgres.sh -set -e +set -eo pipefail host="$1" shift cmd="$@" +timeout=60 +start_time=$(date +%s) -until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$host" -U "postgres" -c '\q'; do +until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$host" -U "$POSTGRES_USER" -c '\q'; do >&2 echo "Postgres is unavailable on ${host} - parse-server is sleeping" sleep 1 + + current_time=$(date +%s) + elapsed_time=$((current_time - start_time)) + + if [ "$elapsed_time" -gt "$timeout" ]; then + >&2 echo "Timed out while waiting for Postgres to become available on ${host}" + exit 1 + fi done ->&2 echo "Postgres is up - executing command" -exec $cmd +>&2 echo "Postgres is up - executing command: $cmd" +exec $cmd \ No newline at end of file