parse-stack-next 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Parse
|
|
7
|
+
module API
|
|
8
|
+
# Helpers for safely interpolating user-controlled segments into REST
|
|
9
|
+
# paths. Every site that builds a request URL via raw string
|
|
10
|
+
# interpolation (`"functions/#{name}"`, `"schemas/#{className}"`, etc.)
|
|
11
|
+
# should route the name through one of these helpers first so a caller
|
|
12
|
+
# passing `"../classes/_User?where=%7B%7D"` cannot traverse to a
|
|
13
|
+
# different endpoint and read it with whatever credentials the outer
|
|
14
|
+
# request was authorized to send.
|
|
15
|
+
module PathSegment
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Parse identifier pattern: starts with a letter or underscore (Parse
|
|
19
|
+
# uses leading underscore for system classes like `_User`,
|
|
20
|
+
# `_Session`, `_Role`), then alphanumerics and underscores. Matches
|
|
21
|
+
# the documented Parse class/field/function/job naming rules.
|
|
22
|
+
IDENTIFIER_PATTERN = /\A[A-Za-z_][A-Za-z0-9_]*\z/.freeze
|
|
23
|
+
|
|
24
|
+
# Validate a Parse identifier (class name, function name, job name,
|
|
25
|
+
# field name) and return it unchanged. Identifiers are already
|
|
26
|
+
# path-safe under the strict pattern, so no percent-encoding is
|
|
27
|
+
# needed; we just refuse anything that violates the shape.
|
|
28
|
+
#
|
|
29
|
+
# @param value the identifier to validate (anything responding to
|
|
30
|
+
# `to_s`).
|
|
31
|
+
# @param kind [String] human-readable name for error messages.
|
|
32
|
+
# @return [String] the validated identifier.
|
|
33
|
+
# @raise [ArgumentError] if blank, contains a slash, contains a dot,
|
|
34
|
+
# or otherwise fails the pattern.
|
|
35
|
+
def identifier!(value, kind: "name")
|
|
36
|
+
s = value.to_s
|
|
37
|
+
if s.empty?
|
|
38
|
+
raise ArgumentError, "#{kind} must not be empty"
|
|
39
|
+
end
|
|
40
|
+
unless IDENTIFIER_PATTERN.match?(s)
|
|
41
|
+
raise ArgumentError,
|
|
42
|
+
"#{kind} #{s.inspect} contains characters that are not allowed in " \
|
|
43
|
+
"a Parse identifier. Names must match /\\A[A-Za-z_][A-Za-z0-9_]*\\z/."
|
|
44
|
+
end
|
|
45
|
+
s
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Validate and percent-encode a less-restrictive path segment, used
|
|
49
|
+
# for file names which can contain hyphens, periods, and other
|
|
50
|
+
# filename-safe characters but must never contain a literal `/`,
|
|
51
|
+
# `..`, or NUL/control characters.
|
|
52
|
+
#
|
|
53
|
+
# @param value the segment to validate.
|
|
54
|
+
# @param kind [String] human-readable name for error messages.
|
|
55
|
+
# @return [String] percent-encoded segment safe for path interpolation.
|
|
56
|
+
# @raise [ArgumentError] if blank, contains a slash, is a path-
|
|
57
|
+
# traversal token, or contains control characters.
|
|
58
|
+
def file!(value, kind: "filename")
|
|
59
|
+
s = value.to_s
|
|
60
|
+
if s.empty?
|
|
61
|
+
raise ArgumentError, "#{kind} must not be empty"
|
|
62
|
+
end
|
|
63
|
+
if s.include?("/") || s == ".." || s == "."
|
|
64
|
+
raise ArgumentError,
|
|
65
|
+
"#{kind} #{s.inspect} contains path-traversal characters " \
|
|
66
|
+
"(`/`, `.`, or `..`). Names must be a single path segment."
|
|
67
|
+
end
|
|
68
|
+
if s.match?(/[\x00-\x1F\x7F]/)
|
|
69
|
+
raise ArgumentError, "#{kind} #{s.inspect} contains control characters"
|
|
70
|
+
end
|
|
71
|
+
URI.encode_www_form_component(s)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
module API
|
|
6
|
+
# Defines the Parse Push notification service interface for the Parse REST API
|
|
7
|
+
module Push
|
|
8
|
+
# @!visibility private
|
|
9
|
+
PUSH_PATH = "push"
|
|
10
|
+
|
|
11
|
+
# Update the schema for a collection.
|
|
12
|
+
# @param payload [Hash] the paylod for the Push notification.
|
|
13
|
+
# @return [Parse::Response]
|
|
14
|
+
# @see http://docs.parseplatform.org/rest/guide/#sending-pushes Sending Pushes
|
|
15
|
+
def push(payload = {})
|
|
16
|
+
request :post, PUSH_PATH, body: payload.as_json
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
module API
|
|
6
|
+
# Defines the Schema interface for the Parse REST API
|
|
7
|
+
module Schema
|
|
8
|
+
# @!visibility private
|
|
9
|
+
SCHEMAS_PATH = "schemas"
|
|
10
|
+
|
|
11
|
+
# Get all the schemas for the application.
|
|
12
|
+
# @param opts [Hash] additional options for the request.
|
|
13
|
+
# @return [Parse::Response]
|
|
14
|
+
def schemas(opts = {})
|
|
15
|
+
request_opts = { cache: false }.merge(opts)
|
|
16
|
+
request :get, SCHEMAS_PATH, opts: request_opts
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Get the schema for a collection.
|
|
20
|
+
# @param className [String] the name of the remote Parse collection.
|
|
21
|
+
# @return [Parse::Response]
|
|
22
|
+
def schema(className)
|
|
23
|
+
safe = Parse::API::PathSegment.identifier!(className, kind: "class name")
|
|
24
|
+
opts = { cache: false }
|
|
25
|
+
request :get, "#{SCHEMAS_PATH}/#{safe}", opts: opts
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create a new collection with the specific schema.
|
|
29
|
+
# @param className [String] the name of the remote Parse collection.
|
|
30
|
+
# @param schema [Hash] the schema hash. This is a specific format specified by
|
|
31
|
+
# Parse.
|
|
32
|
+
# @return [Parse::Response]
|
|
33
|
+
def create_schema(className, schema)
|
|
34
|
+
safe = Parse::API::PathSegment.identifier!(className, kind: "class name")
|
|
35
|
+
request :post, "#{SCHEMAS_PATH}/#{safe}", body: schema
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Update the schema for a collection.
|
|
39
|
+
# @param className [String] the name of the remote Parse collection.
|
|
40
|
+
# @param schema [Hash] the schema hash. This is a specific format specified by
|
|
41
|
+
# Parse.
|
|
42
|
+
# @return [Parse::Response]
|
|
43
|
+
def update_schema(className, schema)
|
|
44
|
+
safe = Parse::API::PathSegment.identifier!(className, kind: "class name")
|
|
45
|
+
request :put, "#{SCHEMAS_PATH}/#{safe}", body: schema
|
|
46
|
+
end
|
|
47
|
+
end #Schema
|
|
48
|
+
end #API
|
|
49
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
module API
|
|
6
|
+
# APIs related to the open source Parse Server.
|
|
7
|
+
module Server
|
|
8
|
+
|
|
9
|
+
# @!attribute server_info
|
|
10
|
+
# @return [Hash] the information about the server.
|
|
11
|
+
attr_writer :server_info
|
|
12
|
+
|
|
13
|
+
# @!visibility private
|
|
14
|
+
SERVER_INFO_PATH = "serverInfo"
|
|
15
|
+
# @!visibility private
|
|
16
|
+
SERVER_HEALTH_PATH = "health"
|
|
17
|
+
# Fetch and cache information about the Parse server configuration. This
|
|
18
|
+
# hash contains information specifically to the configuration of the running
|
|
19
|
+
# parse server.
|
|
20
|
+
# @return (see #server_info!)
|
|
21
|
+
def server_info
|
|
22
|
+
return @server_info if @server_info.present?
|
|
23
|
+
response = request :get, SERVER_INFO_PATH
|
|
24
|
+
@server_info = response.error? ? nil :
|
|
25
|
+
response.result.with_indifferent_access
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Fetches the status of the server based on the health check.
|
|
29
|
+
# @return [Boolean] whether the server is 'OK'.
|
|
30
|
+
def server_health
|
|
31
|
+
opts = { cache: false }
|
|
32
|
+
response = request :get, SERVER_HEALTH_PATH, opts: opts
|
|
33
|
+
response.success?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Force fetches the server information.
|
|
37
|
+
# @return [Hash] a hash containing server configuration if available.
|
|
38
|
+
def server_info!
|
|
39
|
+
@server_info = nil
|
|
40
|
+
server_info
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns the version of the Parse server the client is connected to.
|
|
44
|
+
# @return [String] a version string (ex. '2.2.25') if available.
|
|
45
|
+
def server_version
|
|
46
|
+
server_info.present? ? @server_info[:parseServerVersion] : nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
module API
|
|
6
|
+
# Defines the Session class interface for the Parse REST API
|
|
7
|
+
module Sessions
|
|
8
|
+
# @!visibility private
|
|
9
|
+
SESSION_PATH_PREFIX = "sessions"
|
|
10
|
+
|
|
11
|
+
# Fetch a session record for a given session token.
|
|
12
|
+
# @param session_token [String] an active session token.
|
|
13
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
14
|
+
# @return [Parse::Response]
|
|
15
|
+
def fetch_session(session_token, **opts)
|
|
16
|
+
opts.merge!({ use_master_key: false, cache: false })
|
|
17
|
+
headers = { Parse::Protocol::SESSION_TOKEN => session_token }
|
|
18
|
+
response = request :get, "#{SESSION_PATH_PREFIX}/me", headers: headers, opts: opts
|
|
19
|
+
response.parse_class = Parse::Model::CLASS_SESSION
|
|
20
|
+
response
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "open-uri"
|
|
5
|
+
|
|
6
|
+
module Parse
|
|
7
|
+
module API
|
|
8
|
+
# Defines the User class interface for the Parse REST API
|
|
9
|
+
module Users
|
|
10
|
+
# @!visibility private
|
|
11
|
+
USER_PATH_PREFIX = "users"
|
|
12
|
+
# @!visibility private
|
|
13
|
+
LOGOUT_PATH = "logout"
|
|
14
|
+
# @!visibility private
|
|
15
|
+
LOGIN_PATH = "login"
|
|
16
|
+
# @!visibility private
|
|
17
|
+
REQUEST_PASSWORD_RESET = "requestPasswordReset"
|
|
18
|
+
|
|
19
|
+
# Fetch a {Parse::User} for a given objectId.
|
|
20
|
+
# @param id [String] the user objectid
|
|
21
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
22
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
23
|
+
# @return [Parse::Response]
|
|
24
|
+
def fetch_user(id, headers: {}, **opts)
|
|
25
|
+
request :get, "#{USER_PATH_PREFIX}/#{id}", headers: headers, opts: opts
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Find users matching a set of constraints.
|
|
29
|
+
# @param query [Hash] query parameters.
|
|
30
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
31
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
32
|
+
# @return [Parse::Response]
|
|
33
|
+
def find_users(query = {}, headers: {}, **opts)
|
|
34
|
+
response = request :get, USER_PATH_PREFIX, query: query, headers: headers, opts: opts
|
|
35
|
+
response.parse_class = Parse::Model::CLASS_USER
|
|
36
|
+
response
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Find user matching this active session token.
|
|
40
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
41
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
42
|
+
# @return [Parse::Response]
|
|
43
|
+
def current_user(session_token, headers: {}, **opts)
|
|
44
|
+
headers.merge!({ Parse::Protocol::SESSION_TOKEN => session_token })
|
|
45
|
+
response = request :get, "#{USER_PATH_PREFIX}/me", headers: headers, opts: opts
|
|
46
|
+
response.parse_class = Parse::Model::CLASS_USER
|
|
47
|
+
response
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Create a new user.
|
|
51
|
+
# @param body [Hash] a hash of values related to your _User schema.
|
|
52
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
53
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
54
|
+
# @return [Parse::Response]
|
|
55
|
+
def create_user(body, headers: {}, **opts)
|
|
56
|
+
headers.merge!({ Parse::Protocol::REVOCABLE_SESSION => "1" })
|
|
57
|
+
if opts[:session_token].present?
|
|
58
|
+
headers.merge!({ Parse::Protocol::SESSION_TOKEN => opts[:session_token] })
|
|
59
|
+
end
|
|
60
|
+
response = request :post, USER_PATH_PREFIX, body: body, headers: headers, opts: opts
|
|
61
|
+
response.parse_class = Parse::Model::CLASS_USER
|
|
62
|
+
response
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Update a {Parse::User} record given an objectId.
|
|
66
|
+
# @param id [String] the Parse user objectId.
|
|
67
|
+
# @param body [Hash] the body of the API request.
|
|
68
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
69
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
70
|
+
# @return [Parse::Response]
|
|
71
|
+
def update_user(id, body = {}, headers: {}, **opts)
|
|
72
|
+
response = request :put, "#{USER_PATH_PREFIX}/#{id}", body: body, opts: opts
|
|
73
|
+
response.parse_class = Parse::Model::CLASS_USER
|
|
74
|
+
response
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Set the authentication service OAUth data for a user. Deleting or unlinking
|
|
78
|
+
# is done by setting the authData of the service name to nil.
|
|
79
|
+
# @param id [String] the Parse user objectId.
|
|
80
|
+
# @param service_name [Symbol] the name of the OAuth service.
|
|
81
|
+
# @param auth_data [Hash] the hash data related to the third-party service.
|
|
82
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
83
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
84
|
+
# @return [Parse::Response]
|
|
85
|
+
def set_service_auth_data(id, service_name, auth_data, headers: {}, **opts)
|
|
86
|
+
body = { authData: { service_name => auth_data } }
|
|
87
|
+
update_user(id, body, headers: headers, **opts)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Delete a {Parse::User} record given an objectId.
|
|
91
|
+
# @param id [String] the Parse user objectId.
|
|
92
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
93
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
94
|
+
# @return [Parse::Response]
|
|
95
|
+
def delete_user(id, headers: {}, **opts)
|
|
96
|
+
request :delete, "#{USER_PATH_PREFIX}/#{id}", headers: headers, opts: opts
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Request a password reset for a registered email.
|
|
100
|
+
# @param email [String] the Parse user email.
|
|
101
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
102
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
103
|
+
# @return [Parse::Response]
|
|
104
|
+
def request_password_reset(email, headers: {}, **opts)
|
|
105
|
+
body = { email: email }
|
|
106
|
+
request :post, REQUEST_PASSWORD_RESET, body: body, opts: opts, headers: headers
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Login a user. Implements client-side rate limiting with exponential
|
|
110
|
+
# backoff after repeated failures to mitigate brute force attacks.
|
|
111
|
+
# @param username [String] the Parse user username.
|
|
112
|
+
# @param password [String] the Parse user's associated password.
|
|
113
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
114
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
115
|
+
# @return [Parse::Response]
|
|
116
|
+
def login(username, password, headers: {}, **opts)
|
|
117
|
+
check_login_rate_limit!(username)
|
|
118
|
+
body = { username: username, password: password }
|
|
119
|
+
headers.merge!({ Parse::Protocol::REVOCABLE_SESSION => "1" })
|
|
120
|
+
response = request :post, LOGIN_PATH, body: body, headers: headers, opts: opts
|
|
121
|
+
response.parse_class = Parse::Model::CLASS_USER
|
|
122
|
+
track_login_attempt(username, response.success?)
|
|
123
|
+
response
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Login a user with MFA (Multi-Factor Authentication).
|
|
127
|
+
#
|
|
128
|
+
# This method handles Parse Server's MFA adapter which requires both
|
|
129
|
+
# standard credentials AND an MFA token when MFA is enabled for the user.
|
|
130
|
+
#
|
|
131
|
+
# @param username [String] the Parse user username.
|
|
132
|
+
# @param password [String] the Parse user's associated password.
|
|
133
|
+
# @param mfa_token [String] the TOTP code from authenticator app or recovery code.
|
|
134
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
135
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
136
|
+
# @return [Parse::Response]
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# response = client.login_with_mfa("john", "password123", "123456")
|
|
140
|
+
def login_with_mfa(username, password, mfa_token, headers: {}, **opts)
|
|
141
|
+
check_login_rate_limit!(username)
|
|
142
|
+
# Parse Server expects authData to be sent with POST for MFA login
|
|
143
|
+
body = {
|
|
144
|
+
username: username,
|
|
145
|
+
password: password,
|
|
146
|
+
authData: {
|
|
147
|
+
mfa: {
|
|
148
|
+
token: mfa_token,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
headers.merge!({ Parse::Protocol::REVOCABLE_SESSION => "1" })
|
|
153
|
+
response = request :post, LOGIN_PATH, body: body, headers: headers, opts: opts
|
|
154
|
+
response.parse_class = Parse::Model::CLASS_USER
|
|
155
|
+
track_login_attempt(username, response.success?)
|
|
156
|
+
response
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Logout a user by deleting the associated session.
|
|
160
|
+
# @param session_token [String] the Parse user session token to delete.
|
|
161
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
162
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
163
|
+
# @return [Parse::Response]
|
|
164
|
+
def logout(session_token, headers: {}, **opts)
|
|
165
|
+
headers.merge!({ Parse::Protocol::SESSION_TOKEN => session_token })
|
|
166
|
+
opts.merge!({ use_master_key: false, session_token: session_token })
|
|
167
|
+
request :post, LOGOUT_PATH, headers: headers, opts: opts
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Signup a user given a username, password and, optionally, their email.
|
|
171
|
+
# @param username [String] the Parse user username.
|
|
172
|
+
# @param password [String] the Parse user's associated password.
|
|
173
|
+
# @param email [String] the desired Parse user's email.
|
|
174
|
+
# @param body [Hash] additional property values to pass when creating the user record.
|
|
175
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
176
|
+
# @return [Parse::Response]
|
|
177
|
+
def signup(username, password, email = nil, body: {}, **opts)
|
|
178
|
+
body = body.merge({ username: username, password: password })
|
|
179
|
+
body[:email] = email || body[:email]
|
|
180
|
+
create_user(body, **opts)
|
|
181
|
+
end
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
# @!visibility private
|
|
185
|
+
# Thread-safe tracker for login rate limiting. Keys are usernames, values are
|
|
186
|
+
# { failures: Integer, locked_until: Time }.
|
|
187
|
+
def login_rate_limits
|
|
188
|
+
@login_rate_limit_mutex ||= Mutex.new
|
|
189
|
+
@login_rate_limits ||= {}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Maximum consecutive failures before lockout.
|
|
193
|
+
LOGIN_MAX_FAILURES = 5
|
|
194
|
+
# Base delay in seconds for exponential backoff.
|
|
195
|
+
LOGIN_BASE_DELAY = 2
|
|
196
|
+
# Maximum number of tracked usernames before cleanup.
|
|
197
|
+
LOGIN_RATE_LIMIT_MAX_ENTRIES = 10_000
|
|
198
|
+
# Entries older than this (seconds) are eligible for cleanup.
|
|
199
|
+
LOGIN_RATE_LIMIT_TTL = 600
|
|
200
|
+
|
|
201
|
+
# Checks if a login attempt is allowed for the given username.
|
|
202
|
+
# @raise [RuntimeError] if the account is temporarily locked out.
|
|
203
|
+
def check_login_rate_limit!(username)
|
|
204
|
+
@login_rate_limit_mutex ||= Mutex.new
|
|
205
|
+
@login_rate_limit_mutex.synchronize do
|
|
206
|
+
entry = login_rate_limits[username]
|
|
207
|
+
return unless entry
|
|
208
|
+
if entry[:locked_until] && Time.now < entry[:locked_until]
|
|
209
|
+
wait = (entry[:locked_until] - Time.now).ceil
|
|
210
|
+
raise "Login rate limited for '#{username}'. Try again in #{wait} seconds."
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Records a login attempt result and applies exponential backoff on failure.
|
|
216
|
+
def track_login_attempt(username, success)
|
|
217
|
+
@login_rate_limit_mutex ||= Mutex.new
|
|
218
|
+
@login_rate_limit_mutex.synchronize do
|
|
219
|
+
if success
|
|
220
|
+
login_rate_limits.delete(username)
|
|
221
|
+
else
|
|
222
|
+
entry = login_rate_limits[username] || { failures: 0, locked_until: nil }
|
|
223
|
+
entry[:failures] += 1
|
|
224
|
+
if entry[:failures] >= LOGIN_MAX_FAILURES
|
|
225
|
+
delay = LOGIN_BASE_DELAY**(entry[:failures] - LOGIN_MAX_FAILURES + 1)
|
|
226
|
+
delay = [delay, 300].min # cap at 5 minutes
|
|
227
|
+
entry[:locked_until] = Time.now + delay
|
|
228
|
+
end
|
|
229
|
+
login_rate_limits[username] = entry
|
|
230
|
+
end
|
|
231
|
+
# Periodic cleanup of expired entries to prevent memory leak
|
|
232
|
+
cleanup_login_rate_limits if login_rate_limits.size > LOGIN_RATE_LIMIT_MAX_ENTRIES
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Removes expired entries from the rate limit tracker.
|
|
237
|
+
# Only deletes entries whose lockout has actually expired past the TTL —
|
|
238
|
+
# never deletes pre-lockout failure counters (which would defeat rate limiting
|
|
239
|
+
# by letting an attacker flood random usernames to trigger cleanup and reset
|
|
240
|
+
# a target's in-progress counter).
|
|
241
|
+
def cleanup_login_rate_limits
|
|
242
|
+
now = Time.now
|
|
243
|
+
login_rate_limits.delete_if do |_username, entry|
|
|
244
|
+
entry[:locked_until] && (now - entry[:locked_until]) > LOGIN_RATE_LIMIT_TTL
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
end # Users
|
|
249
|
+
end #API
|
|
250
|
+
end #Parse
|