Compare commits

..
Author SHA1 Message Date
Jake BarnbyandClaude Opus 4.7 ac87c0e2d6 test(notifications): add regression and happy-path coverage for review-fix critical gaps
Locks the bug-fix invariants from PR #12195's last review pass and rounds out
worker channel coverage:

C1 testEmailSendFailureDoesNotPersistAlert — SMTP throw must NOT leave a dedup
   row behind, retry must deliver and persist exactly once.
C2 testNotificationEventResetClearsAllState — reset() drops every state-bearing
   field including preview (regression: missed in original reset body).
C3 testWebhookSendAlertResetsBetweenCalls — Webhooks::sendAlert must reset()
   the DI-shared Notification event so two paused-webhook alerts in one worker
   pass do not bleed recipients/subject/body into each other.
C4 testConsoleAdapterTreatsDuplicateAsDelivered — Duplicate on createDocument
   must surface as a successful idempotent send, not a per-recipient error.
M7 testTrackingPixelRejectsJwtWithoutPurposeClaim — Track endpoint silently
   ignores JWTs missing or with the wrong purpose claim (defends against
   replaying session/reset JWTs to mark alerts read).

Worker happy-path tests: testEmailChannelHappyPath, testConsoleChannelHappyPath,
testWebhookChannelHappyPath cover the full per-channel dispatch contract end
to end, including HMAC signing for webhooks and the tracking pixel injection
+ post-send persistence for email.

Also extracts CapturingWebhook into its own PSR-4 file so reused across tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:02:12 +12:00
Jake Barnby f83993d51e merge: major review fixes (M2 drop SDK auth, M4 dedup unique-index test, M9 e2e secret assertion) 2026-05-06 16:15:27 +12:00
Jake Barnby a0ec2827fa merge: critical review fixes (C1 deferred persist, C2 reset preview, C3 webhook reset, C4 dup catch, M3 status, M5 id helper, M7 jwt purpose) 2026-05-06 16:15:23 +12:00
Jake BarnbyandClaude Opus 4.7 1aa6a05ad8 fix(notifications): drop misleading auth metadata, exercise unique-index dedup, fail loudly on missing tracking secret in tests
- Account/Alerts/Track endpoint is scope:public (email clients have no
  session); SDK Method now declares auth: [] so generators do not require
  an auth header for an unauthenticated endpoint.
- NotificationsTest setUp now creates the production `_key_recipient`
  UNIQUE composite index on (messageId, channel, userId, teamId), and
  testPersistAlertReturnsExistingAlertIdOnDuplicate exercises the
  DuplicateException → return-existing-alertId branch end-to-end (both
  primary-key collision and unique-index collision via a sibling $id).
- Tracking-pixel e2e now asserts _APP_OPENSSL_KEY_V1 is set instead of
  silently falling back to the .env.example placeholder, so a missing
  CI secret fails loudly rather than passing against the wrong key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:13:52 +12:00
Jake BarnbyandClaude Opus 4.7 5f8cc27c9f fix(notifications): defer persistence until after send, reset event state, distinguish duplicate writes, scope tracking jwt purpose
- C1: dispatchEmail now writes the alert row only after a successful
  adapter send. The deterministic alertId is pre-computed via the new
  buildAlertId() helper so the tracking pixel URL stays stable, but the
  database write is deferred. Previously an SMTP failure left an orphan
  dedup row that permanently swallowed the email on retry.
- C2: Notification::reset() now clears $preview alongside the other
  per-trigger fields so state cannot leak across triggers.
- C3: Webhooks::sendAlert() resets the DI-shared notification queue at
  the top, preventing recipients/subject/body accumulation across
  multiple webhook failures in one worker invocation.
- C4: Console adapter catches DuplicateException separately and treats
  it as a successful (already-delivered) result, so an idempotent retry
  no longer surfaces as a delivery failure when combined with
  dispatchConsole's zero-delivery throw.
- M3: dispatchEmail throws plain 500 for any SMTP delivery failure;
  401 mis-signalled auth errors for timeouts/DNS/connection refused.
- M5: buildAlertId() is the single source of truth shared by
  persistAlert() and dispatchEmail(); no more drift between the
  tracking-pixel id and the persisted row's id.
- M7: Tracking JWT now carries a purpose='alert_track' claim and the
  /v1/account/alerts/:alertId/track decoder rejects tokens missing or
  mismatching that purpose, isolating the shared signing key from
  cross-endpoint replay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:06:49 +12:00
Jake Barnby e125bf6aec merge: cross-db alerts persistence fix 2026-05-06 15:26:51 +12:00
Jake BarnbyandClaude Opus 4.7 aa9b45b5a1 fix(notifications): persist alerts to platform db regardless of dispatching project
The `alerts` collection lives in the platform database, but the Notifications
worker was injecting `dbForProject` and writing alerts there. Webhooks
dispatches with the user's project context, so alerts were being written to
the user's project DB (which has no `alerts` collection), and `/v1/account/alerts`
(which reads from `dbForPlatform`) returned nothing.

Switch the worker to inject `dbForPlatform` instead. All alert reads (dedup
lookup) and writes (persistAlert + ConsoleAdapter) now target the platform DB,
independent of which project the dispatching event belongs to.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:25:45 +12:00
Jake Barnby 5357e7dd35 fix(notifications): quote $id key in PHPDoc array shape
PHPStan rejects unquoted $id as a variable token in array shape syntax.
Quoting the key satisfies the parser without changing the runtime contract.
2026-05-06 15:24:25 +12:00
Jake Barnby a8ce7c3736 merge: ST17 e2e tests 2026-05-06 15:11:57 +12:00
Jake Barnby 540a7823cd merge: ST16 unit tests 2026-05-06 15:11:54 +12:00
Jake BarnbyandClaude Opus 4.7 534123cd87 test(notifications): add unit tests for dedup, fanout, console skip, zero-delivery, recipient struct, and tracking pixel
Cover the new worker behaviors introduced in waves 1-3:

- testDedupQueriesByAttributeNotById: prove alreadyDelivered() queries the
  messageId attribute, not getDocument($messageId), by seeding a row with
  a non-matching $id but matching messageId.
- testConsoleChannelSkipsPersistAlert: confirm the action loop does NOT
  call persistAlert for console recipients (the adapter persists).
- testConsoleZeroDeliveryThrows: surface adapter failures via the worker.
- testMultiRecipientFanoutNoCollision: same messageId across recipients
  must produce distinct $id values.
- testRecipientStructRoundtripsUserIdAndTeamId: persisted alert carries
  recipient userId/teamId.
- testTrackingPixelInjectedIntoEmailHtml: verify the tracking pixel is
  spliced before the last </body> tag with the expected URL shape.
- testPersistAlertReturnsAlertIdAndStoresUserId: dispatchEmail's
  persistAlert returns a resolvable id and stores userId + read=false.

ConsoleTest: add testMultiRecipientWithSameMessageIdGeneratesDistinctIds
and update existing tests to use the post-ST2 compound `$id` form
(messageId + 8-hex md5 suffix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:10:38 +12:00
Jake Barnby 3e0df70f5c test(notifications): e2e tests for list, mark-read, tracking pixel, and webhook-failure fanout
Add 6 new test methods to NotificationsBase that exercise the wave3
account-alerts surface end-to-end:

- testListAccountAlertsEmpty: GET /v1/account/alerts shape check
- testWebhookFailureCreatesConsoleAlert: drives a webhook past
  _APP_WEBHOOK_MAX_FAILED_ATTEMPTS via user-create events and polls
  /account/alerts until the worker fans the paused alert out to the
  project owner on the console channel
- testMarkAlertReadTogglesFlag: PATCH /:alertId/read happy path
- testMarkAlertReadUnauthorized: stranger console user cannot mark
  someone else's alert as read; alert remains unread for the owner
- testTrackingPixelTogglesRead: GET /:alertId/track with a valid
  HS256 JWT signed with _APP_OPENSSL_KEY_V1 returns the canonical 1x1
  PNG and atomically marks the alert as read
- testTrackingPixelInvalidTokenReturnsPng: tampered JWT still gets a
  PNG (no information disclosure) but performs no DB write

Helpers:

- seedWebhookFailureAlert: registers a webhook pointing at an
  unroutable address (http://127.0.0.1:1/), drives max+2 user-create
  events through the project, polls assertEventually with a 60s budget
  for the paused alert keyed by the deterministic md5 of
  'webhook:<id>:paused:<attempts>'
- createConsoleUser: spins up a fresh, unrelated console user with its
  own session for the unauthorized assertion
- getConsoleAlertHeaders: console-session auth bundle reused across
  every alerts call, so the trait works identically under SideServer
  and SideConsole hosts
2026-05-06 15:09:28 +12:00
Jake Barnby 080b6dbeb2 merge: ST14 alerts Track body 2026-05-06 14:48:39 +12:00
Jake Barnby 678a6bd079 merge: ST13 alerts XList body 2026-05-06 14:48:36 +12:00
Jake Barnby 3b0c46c85e merge: ST12 PATCH /v1/account/alerts/:alertId/read 2026-05-06 14:48:33 +12:00
Jake Barnby c419257755 merge: ST11 Webhooks worker switch to Notification 2026-05-06 14:48:29 +12:00
Jake BarnbyandClaude Opus 4.7 067bf582dc feat(webhooks): emit pausing alerts via notifications worker to owners on email and console
When a webhook is permanently paused after exceeding the max failure
threshold, fan out the alert through the new Notifications worker
instead of the legacy Mails worker. Recipients are filtered to project
owners only and receive both an email and a console notification, with
a single deduplication key per webhook + attempts so duplicate triggers
collapse downstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:46:57 +12:00
Jake BarnbyandClaude Opus 4.7 dae98bf240 feat(notifications): track email opens by decoding pixel JWT and marking alert read
Replace placeholder body with JWT decode, alertId/userId match validation,
sparse update of read=true via injected Authorization::skip. Always returns
1x1 PNG regardless of decode outcome to avoid leaking JWT validity through
response status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:46:33 +12:00
Jake BarnbyandClaude Opus 4.7 5dd987711b feat(notifications): implement GET /v1/account/alerts list endpoint body
Replace the placeholder body of the listAlerts action with real
implementation: scope alerts to the current user via userId equality,
parse incoming queries, support cursor pagination over the alerts
collection, run filters through count for total, and return
MODEL_ALERT_LIST.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:46:22 +12:00
Jake BarnbyandClaude Opus 4.7 e54fbf3b3f feat(notifications): mark alert read via PATCH /v1/account/alerts/:alertId/read
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:35:53 +12:00
Jake Barnby 0a9b54543d merge: ST10 queueForNotifications DI registration 2026-05-06 14:20:10 +12:00
Jake Barnby 93bdebb7fa merge: ST9 notifications worker fixes 2026-05-06 14:20:07 +12:00
Jake BarnbyandClaude Opus 4.7 6f01c1492f fix(notifications): worker dedup by attribute, throw on console-zero-delivery, thread recipient userId, inject email tracking pixel
Apply the four Greptile P1 fixes to the Notifications worker and
extend it for C3 email read tracking and ST4's stripped SMTP plumbing.

P1 #1: alreadyDelivered() now queries the indexed messageId attribute
instead of getDocument($messageId). The action loop and Console
adapter both write compound `$id`s (messageId + recipient hash), so
the previous direct-id lookup always missed.

P1 #3: action() no longer calls persistAlert after dispatchConsole;
ConsoleAdapter persists internally. Email persists inside
dispatchEmail BEFORE the adapter send so the alertId is available for
the tracking pixel; webhook persists in the action loop after a
successful HTTP send.

P1 #4: dispatchConsole now throws when the adapter reports
`deliveredTo === 0`, surfacing the per-recipient error.

Recipient threading: dispatch() now takes the full recipient map and
returns the alertId (or null when persistence is the caller's
responsibility). persistAlert() reads userId/teamId from the
recipient and grants per-user / per-team-owner CRUD permissions,
falling back to payload permissions only when neither is set. The
returned alertId lets dispatchEmail splice a 1x1 tracking pixel
before the last `</body>` tag, signed with a 30-day HS256 JWT
(_APP_OPENSSL_KEY_V1) carrying {alertId, userId}.

SMTP resolution: ST4 stripped `smtp` and `customMailOptions` from
the Notification event payload, so the worker now resolves SMTP
from the injected project Document (mirroring Mails.php /
Memberships/Create.php), falling back to the env-driven cloud SMTP
adapter when the project has no enabled override.

Tests updated: SpyNotifications.dispatch() matches the new
signature and emulates per-channel persistence so existing routing
assertions keep their semantics. Memory `alerts` collection adds
the `read` boolean attribute to mirror platform.php.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:16:25 +12:00
Jake BarnbyandClaude Opus 4.7 4e7236a366 feat(notifications): register queueForNotifications resource in worker and request containers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:00:45 +12:00
Jake BarnbyandClaude Opus 4.7 8c7ecd6cdb fix(notifications): add SDK Method group to alerts skeleton actions
PHPStan flagged missing required `group` parameter on the SDK Method
constructors for the alerts XList and Track\Get skeleton actions.
Group set to 'alerts' to align with the resource grouping convention
used elsewhere in the account namespace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:57:07 +12:00
Jake Barnby 0f1a2b5097 merge: ST4 event recipient + SMTP removal
# Conflicts:
#	src/Appwrite/Event/Notification.php
2026-05-06 13:48:47 +12:00
Jake Barnby a02eb3f41c merge: ST1 NOTIFICATION_TYPE_* rename 2026-05-06 13:48:16 +12:00
Jake Barnby ddffc69914 merge: ST8 alerts track skeleton
# Conflicts:
#	src/Appwrite/Platform/Modules/Account/Services/Http.php
2026-05-06 13:48:10 +12:00
Jake Barnby 439b3db2bf merge: ST7 alerts XList skeleton 2026-05-06 13:47:05 +12:00
Jake Barnby 134a8710da merge: ST6 alerts queries validator 2026-05-06 13:46:59 +12:00
Jake Barnby b9a928d3f8 merge: ST5 alert response model 2026-05-06 13:46:53 +12:00
Jake Barnby d07292ef1f merge: ST3 alerts schema 2026-05-06 13:46:49 +12:00
Jake Barnby 8cafc8c542 merge: ST2 console $id collision fix 2026-05-06 13:46:46 +12:00
Jake BarnbyandClaude Opus 4.7 a0400d2e93 feat(notifications): scaffold GET /v1/account/alerts/:alertId/track endpoint
Adds a public-scope tracking-pixel endpoint skeleton that always returns a
1x1 transparent PNG. The body is intentionally minimal so the email-tracking
read-flag flip can be wired up cleanly in Wave 3 (ST14) once the JWT decode
and DB write paths are designed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:44:58 +12:00
Jake BarnbyandClaude Opus 4.7 d79be3a6e3 feat(notifications): add alert and alertList response models
Adds Alert response model and MODEL_ALERT/MODEL_ALERT_LIST constants
so future GET /v1/account/alerts endpoint can return MODEL_ALERT_LIST.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:44:27 +12:00
Jake BarnbyandClaude Opus 4.7 55275259d9 feat(notifications): scaffold GET /v1/account/alerts endpoint
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:44:09 +12:00
Jake BarnbyandClaude Opus 4.7 2c2e920a1a feat(notifications): add alerts queries validator
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:42:23 +12:00
Jake BarnbyandClaude Opus 4.7 4351dffb88 refactor(notifications): drop SMTP plumbing from event and extend recipient struct with userId/teamId
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:42:22 +12:00
Jake BarnbyandClaude Opus 4.7 a5587ff096 feat(notifications): track read state and enable per-recipient alert idempotency
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:42:10 +12:00
Jake BarnbyandClaude Opus 4.7 3df219df66 refactor(notifications): consolidate channel constants under NOTIFICATION_TYPE_* prefix
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:41:59 +12:00
Jake BarnbyandClaude Opus 4.7 36e1615498 fix(notifications): suffix Console adapter $id with recipient hash to avoid multi-recipient collision
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:41:11 +12:00
Jake BarnbyandClaude Opus 4.7 0ce3978f2c feat(notifications): add worker entrypoint and registration alongside Mails
Adds the bin/worker-notifications entrypoint, Dockerfile chmod, and
docker-compose service definitions for the new Notifications worker
without disturbing the existing Mails worker, and re-adds the Mails
queue/class constants on Event so the legacy callers still resolve.
The full mail->notification swap (caller migration, Mails removal,
worker rename) will land as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:56:43 +12:00
Jake Barnby d6be6e165b test(notifications): split e2e into base + server/console overlays
Mirrors the dominant e2e pattern (FunctionsBase/MigrationsBase): the
shared health-queue assertions live in a NotificationsBase trait,
with thin per-side overlays for ProjectCustom + SideServer and
ProjectCustom + SideConsole.
2026-05-01 15:51:25 +12:00
Jake Barnby 7839b3bb3f refactor(notifications): align webhook signing with per-recipient signatureKey
Drop the global _APP_NOTIFICATIONS_WEBHOOK_SECRET env var. There's no
analogous global webhook secret in Appwrite; the existing Webhooks
worker carries a per-webhook signatureKey on the webhook document.

Move the same pattern into the Notification event: each webhook
recipient may carry an optional signatureKey, which the worker
forwards to the Webhook adapter for HMAC-SHA256 signing. Recipients
without a key are delivered unsigned and a tag is logged for audit.
2026-05-01 15:51:25 +12:00
Jake Barnby 5320c9441b refactor(notifications): rename dedupKey to deduplicationKey
No abbreviations in identifier names.
2026-05-01 15:51:25 +12:00
Jake Barnby 8ad73aa6bb refactor(notifications): use Utopia Fetch client in Webhook adapter
Replace raw curl with Utopia\Fetch\Client to align with the rest of
the codebase (Github OAuth adapter, Install task). The dispatch seam
is preserved so existing tests can still substitute responses.
2026-05-01 15:51:25 +12:00
Jake Barnby 5cbbacaae5 refactor(notifications): move adapters under Appwrite\Utopia\Messaging
Mirror the upstream Utopia\Messaging package namespace under the
Appwrite\Utopia\Messaging prefix, matching the convention used by
Appwrite\Utopia\Database and Appwrite\Utopia\Response.
2026-05-01 15:51:25 +12:00
Jake Barnby 5ee92e1844 fix(notifications): move alerts collection to platform DB
Alerts is a platform-DB-only concern. Move from common.php to
platform.php where it belongs alongside other platform collections.
2026-05-01 15:51:25 +12:00
Jake BarnbyandClaude Opus 4.7 a546c41662 docs(notifications): document _APP_NOTIFICATIONS_WEBHOOK_SECRET
Registers the webhook signing secret in app/config/variables.php under a
new Notifications category, threads it through the worker container in
docker-compose.yml and tests/resources/docker/docker-compose.yml, and
adds an empty default to .env so operators see the knob alongside the
existing SMTP block.

When set, outbound webhook deliveries from the notifications worker
include an X-Appwrite-Webhook-Signature header carrying
sha256=<hex(hmac_sha256(timestamp.body, secret))>. When unset, the
worker delivers payloads unsigned — receivers must decide whether to
accept them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:51:25 +12:00
Jake BarnbyandClaude Opus 4.7 df16c84b51 test(notifications): e2e coverage for notifications queue health
Adds the Notifications e2e suite under tests/e2e/Services/Notifications.
Asserts that the live notifications queue depth is reported via
GET /v1/health/queue/notifications, that the threshold guard is honoured,
and that the failed-jobs endpoint accepts the v1-notifications queue name.

Dispatch routing, dedup, and webhook signing are covered by the unit
suite — the worker cannot be deterministically driven through the live
queue from a test client without an admin enqueue endpoint, so the e2e
file pins the public health contract that ops dashboards and KEDA scale
on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:51:24 +12:00
Jake BarnbyandClaude Opus 4.7 6bde54675e test(notifications): unit tests for worker and adapters
Covers the dedup short-circuit, per-channel dispatch routing, alert
persistence, error tagging, and legacy single-recipient fallback in the
Notifications worker, plus the Console adapter's permission shape and the
Webhook adapter's HMAC-SHA256 signing contract, header layout, response
handling, and unsigned-when-secret-missing behaviour.

Worker dispatch helpers move from private to protected so a test spy can
override them without monkey-patching. The Swoole runtime hook flag
mutation is now guarded by class_exists so the action can run under bare
PHPUnit (no Swoole extension on the test host).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:51:24 +12:00
Jake Barnby 90ef2d7487 feat(notifications): add Notifications worker and register it
Replaces the Mails worker. Resolves recipients into email, console, and
webhook channels, deduplicates deliveries via a hashed dedupKey, and persists
alerts for the console channel.
2026-05-01 12:07:42 +12:00
Jake Barnby 725ba86239 feat(notifications): add notifications schema and channel constants
Adds the alerts collection used by the console channel, plus channel and
provider type constants shared by the worker and adapters.
2026-05-01 12:07:40 +12:00
Jake Barnby 99641f24a5 feat(notifications): add Console and Webhook provider adapters
Console adapter persists alerts directly to the project database. Webhook
adapter dispatches signed JSON payloads (HMAC-SHA256) to subscriber URLs.
2026-05-01 12:07:38 +12:00
Jake Barnby 34935b8eed feat(notifications): add Notification event class
Replaces the Mail event with a multi-channel Notification event capable of
dispatching to email, console, and webhook recipients in a single payload.
Maintains the existing Mail surface (setSubject, setRecipient, setBody, etc.)
so existing call sites can be migrated mechanically.
2026-05-01 12:07:32 +12:00
565 changed files with 11377 additions and 23086 deletions
+1 -1
View File
@@ -25,6 +25,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: AI Moderator
uses: github/ai-moderator@81159c370785e295c97461ade67d7c33576e9319 # v1.1.4
uses: github/ai-moderator@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Issue Labeler
uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4
uses: github/issue-labeler@v3.4
with:
configuration-path: .github/labeler.yml
enable-versioned-regex: false
+64 -58
View File
@@ -32,7 +32,7 @@ jobs:
actions: read
security-events: write
contents: read
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@c51854704019a247608d928f370c98740469d4b5" # v2.3.5
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.3.3"
security:
name: Checks / Image
@@ -43,13 +43,13 @@ jobs:
security-events: write
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: 'recursive'
- name: Build the Docker image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@v6
with:
context: .
push: false
@@ -58,7 +58,7 @@ jobs:
target: production
- name: Run Trivy vulnerability scanner on image
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
uses: aquasecurity/trivy-action@0.35.0
with:
image-ref: 'pr_image:${{ github.sha }}'
format: 'sarif'
@@ -66,7 +66,7 @@ jobs:
severity: 'CRITICAL,HIGH'
- name: Run Trivy vulnerability scanner on source code
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
uses: aquasecurity/trivy-action@0.35.0
with:
scan-type: 'fs'
scan-ref: '.'
@@ -76,14 +76,14 @@ jobs:
skip-setup-trivy: true
- name: Upload image scan results
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-image-results.sarif') != ''
with:
sarif_file: 'trivy-image-results.sarif'
category: 'trivy-image'
- name: Upload source code scan results
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-fs-results.sarif') != ''
with:
sarif_file: 'trivy-fs-results.sarif'
@@ -94,10 +94,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
@@ -119,7 +119,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
fetch-depth: 2
@@ -127,7 +127,7 @@ jobs:
if: github.event_name == 'pull_request'
- name: Setup PHP
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
@@ -144,10 +144,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
@@ -157,7 +157,7 @@ jobs:
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Cache PHPStan result cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@v4
with:
path: .phpstan-cache
key: phpstan-${{ github.sha }}
@@ -172,10 +172,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: swoole
@@ -193,10 +193,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v4
with:
node-version: '24'
@@ -212,7 +212,7 @@ jobs:
steps:
- name: Generate matrix
id: generate
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
with:
script: |
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
@@ -253,28 +253,28 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
submodules: recursive
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@v4
- name: Build and push Appwrite
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@v6
with:
context: .
push: true
@@ -297,16 +297,16 @@ jobs:
packages: read
steps:
- name: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -327,7 +327,7 @@ jobs:
run: docker compose exec -T appwrite vars
- name: Run Unit Tests
uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -350,16 +350,16 @@ jobs:
packages: read
steps:
- name: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -385,7 +385,7 @@ jobs:
done
- name: Run General Tests
uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -406,7 +406,7 @@ jobs:
e2e_service:
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
runs-on: ${{ matrix.runner || format('runs-on={0}/runner=4cpu-linux-x64/volume=120g/spot=false', github.run_id) }}
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
needs: [build, matrix]
permissions:
contents: read
@@ -427,7 +427,6 @@ jobs:
FunctionsSchedule,
GraphQL,
Health,
Advisor,
Locale,
Projects,
Realtime,
@@ -442,23 +441,30 @@ jobs:
VCS,
Messaging,
Migrations,
Project,
Presences
Project
]
include:
- service: Databases
runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/volume=120g/spot=false
runner: blacksmith-4vcpu-ubuntu-2404
paratest_processes: 3
timeout_minutes: 30
- service: Sites
runner: blacksmith-4vcpu-ubuntu-2404
- service: Functions
runner: blacksmith-4vcpu-ubuntu-2404
- service: Avatars
runner: blacksmith-4vcpu-ubuntu-2404
- service: Realtime
runner: blacksmith-4vcpu-ubuntu-2404
- service: TablesDB
runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/volume=120g/spot=false
runner: blacksmith-4vcpu-ubuntu-2404
paratest_processes: 3
timeout_minutes: 30
- service: Migrations
paratest_processes: 1
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
- name: Set environment
run: |
@@ -482,13 +488,13 @@ jobs:
fi
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -519,7 +525,7 @@ jobs:
done
- name: Run tests
uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -567,18 +573,18 @@ jobs:
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -601,7 +607,7 @@ jobs:
docker compose up -d --quiet-pull --wait
- name: Run tests
uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -634,16 +640,16 @@ jobs:
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -673,7 +679,7 @@ jobs:
done
- name: Run tests
uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -705,18 +711,18 @@ jobs:
packages: read
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -729,7 +735,7 @@ jobs:
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}:after
- name: Setup k6
uses: grafana/setup-k6-action@db07bd9765aac508ef18982e52ab937fe633a065 # v1.2.1
uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2
with:
k6-version: ${{ env.K6_VERSION }}
@@ -768,7 +774,7 @@ jobs:
- name: Benchmark before
if: steps.benchmark_before_start.outcome == 'success'
continue-on-error: true
uses: grafana/run-k6-action@de51a7390bdf0ac85a3bef493691bd71d4c7c158 # v1.4.0
uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
env:
APPWRITE_ENDPOINT: 'http://localhost/v1'
APPWRITE_BENCHMARK_ITERATIONS: '5'
@@ -820,7 +826,7 @@ jobs:
- name: Benchmark after
id: benchmark_after
continue-on-error: true
uses: grafana/run-k6-action@de51a7390bdf0ac85a3bef493691bd71d4c7c158 # v1.4.0
uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
env:
APPWRITE_ENDPOINT: 'http://localhost/v1'
APPWRITE_BENCHMARK_ITERATIONS: '5'
@@ -840,7 +846,7 @@ jobs:
- name: Comment on PR
if: always()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v8
env:
BENCHMARK_BASE_REF: ${{ github.event.pull_request.base.ref }}
BENCHMARK_HEAD_REF: ${{ github.event.pull_request.head.ref }}
@@ -850,7 +856,7 @@ jobs:
await comment({ github, context, core });
- name: Save results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: benchmark-results
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
- name: Cleanup
run: |
+4 -4
View File
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -47,14 +47,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -68,4 +68,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/analyze@v2
+6 -6
View File
@@ -10,13 +10,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
submodules: recursive
- name: Build the Docker image
run: DOCKER_BUILDKIT=1 docker build . --target production -t appwrite_image:latest
- name: Run Trivy vulnerability scanner on image
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
image-ref: 'appwrite_image:latest'
format: 'sarif'
@@ -24,7 +24,7 @@ jobs:
ignore-unfixed: 'false'
severity: 'CRITICAL,HIGH'
- name: Upload Docker Image Scan Results
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-image-results.sarif') != ''
with:
sarif_file: 'trivy-image-results.sarif'
@@ -35,16 +35,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
- name: Run Trivy vulnerability scanner on filesystem
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
scan-type: 'fs'
format: 'sarif'
output: 'trivy-fs-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Code Scan Results
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-fs-results.sarif') != ''
with:
sarif_file: 'trivy-fs-results.sarif'
+6 -6
View File
@@ -12,33 +12,33 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
fetch-depth: 2
submodules: recursive
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
uses: docker/metadata-action@v4
with:
images: appwrite/cloud
tags: |
type=ref,event=tag
- name: Build & Publish to DockerHub
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
+6 -6
View File
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -20,20 +20,20 @@ jobs:
submodules: recursive
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
uses: docker/metadata-action@v4
with:
images: appwrite/appwrite
tags: |
@@ -42,7 +42,7 @@ jobs:
type=semver,pattern={{major}}
- name: Build and push
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
- name: Set SDK type
id: set-sdk
@@ -49,7 +49,7 @@ jobs:
docker compose exec appwrite sdks --platform=${{ steps.set-sdk.outputs.platform }} --sdk=${{ steps.set-sdk.outputs.sdk_type }} --version=latest --git=no
sudo chown -R $USER:$USER ./app/sdks/${{ steps.set-sdk.outputs.platform }}-${{ steps.set-sdk.outputs.sdk_type }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/setup-node@v4
with:
node-version: 20
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v6
with:
submodules: recursive
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
- uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue has been labeled as a 'question', indicating that it requires additional information from the requestor. It has been inactive for 7 days. If no further activity occurs, this issue will be closed in 14 days."
-8
View File
@@ -115,14 +115,6 @@ Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `
- Never hardcode credentials -- use environment variables.
- Code changes may require container restart. No central log location -- check relevant containers.
## Tracing with Utopia Span
In handlers, only call `Span::add($key, $value)`. **Never** call `Span::init`, `Span::error`, or `Span::finish` -- lifecycle is owned by the entry-point harness (`app/http.php`, `app/worker.php`, `app/realtime.php`, `Bus::dispatch`). For selective export, filter in the sampler in `app/init/span.php`.
Keys are `snake_case` with dots only for child relationships: `project.id` (id of project), `storage.bucket.id`. No dot otherwise: `inbound_bytes`, not `inbound.bytes`. No camelCase, no bare top-level keys (`function.id`, not `functionId`).
Cross-cutting identifiers (`project.id`, `function.id`, `user.id`) live at the top level, not under a subsystem (no `realtime.project.id`). The trace sampler and downstream filters look them up by the canonical key.
## Patch release process
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
+2 -1
View File
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM appwrite/base:1.4.1 AS base
FROM appwrite/base:1.2.1 AS base
LABEL maintainer="team@appwrite.io"
@@ -93,6 +93,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/worker-functions && \
chmod +x /usr/local/bin/worker-mails && \
chmod +x /usr/local/bin/worker-messaging && \
chmod +x /usr/local/bin/worker-notifications && \
chmod +x /usr/local/bin/worker-migrations && \
chmod +x /usr/local/bin/worker-webhooks && \
chmod +x /usr/local/bin/worker-stats-usage && \
+54 -2
View File
@@ -2,10 +2,17 @@
require_once __DIR__ . '/init.php';
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Appwrite\Usage\Context as UsageContext;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Swoole\Timer;
use Utopia\Cache\Adapter\Pool as CachePool;
@@ -19,12 +26,17 @@ use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Service;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Registry\Registry;
use Utopia\System\System;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
use function Swoole\Coroutine\run;
@@ -35,7 +47,6 @@ Config::setParam('runtimes', (new Runtimes('v5'))->getAll(supported: false));
require_once __DIR__ . '/controllers/general.php';
global $register;
global $container;
$platform = new Appwrite();
$args = $_SERVER['argv'] ?? [];
@@ -47,6 +58,7 @@ if (! isset($args[0])) {
}
$taskName = $args[0];
$container = new Container();
$cli = new CLI(new Generic(), $_SERVER['argv'] ?? [], $container);
$platform->setCli($cli);
@@ -119,6 +131,10 @@ $container->set('dbForPlatform', function ($pools, $cache, $authorization) {
return $dbForPlatform;
}, ['pools', 'cache', 'authorization']);
$container->set('console', function () {
return new Document(Config::getParam('console'));
}, []);
$container->set(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false,
@@ -235,10 +251,42 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
return $database;
};
}, ['pools', 'cache', 'authorization']);
$container->set('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
$container->set('publisherDatabases', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherFunctions', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('usage', function () {
return new UsageContext();
}, []);
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
), ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
@@ -291,10 +339,14 @@ $container->set('logError', function (Registry $register) {
};
}, ['register']);
$container->set('executor', fn () => new Executor(), []);
$container->set('bus', function (Registry $register) use ($container) {
return $register->get('bus')->setResolver(fn (string $name) => $container->get($name));
}, ['register']);
$container->set('telemetry', fn () => new NoTelemetry(), []);
$exitCode = 0;
$cli
-7
View File
@@ -1523,13 +1523,6 @@ return [
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_team_confirm'),
'type' => Database::INDEX_KEY,
'attributes' => ['teamInternalId', 'confirm'],
'lengths' => [],
'orders' => [],
],
],
],
+138 -449
View File
@@ -404,13 +404,6 @@ $platformCollections = [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_teamInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['teamInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
],
@@ -642,13 +635,6 @@ $platformCollections = [
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_project_id'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
],
@@ -1021,9 +1007,146 @@ $platformCollections = [
'attributes' => ['projectInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
]
],
],
'alerts' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('alerts'),
'name' => 'Alerts',
'attributes' => [
[
'$id' => ID::custom('messageId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('_key_project_id'),
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => false,
'default' => 'info',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('channel'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('teamId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('title'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('body'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('read'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => false,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_messageId'),
'type' => Database::INDEX_KEY,
'attributes' => ['messageId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_recipient'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['messageId', 'channel', 'userId', 'teamId'],
'lengths' => [Database::LENGTH_KEY, 64, Database::LENGTH_KEY, Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC, Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_userId_read'),
'type' => Database::INDEX_KEY,
'attributes' => ['userId', 'read'],
'lengths' => [Database::LENGTH_KEY, 0],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_teamId_read'),
'type' => Database::INDEX_KEY,
'attributes' => ['teamId', 'read'],
'lengths' => [Database::LENGTH_KEY, 0],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_projectId'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'lengths' => [Database::LENGTH_KEY],
@@ -1956,440 +2079,6 @@ $platformCollections = [
'attributes' => [],
'indexes' => []
],
'reports' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('reports'),
'name' => 'Reports',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('appInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('appId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('title'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('summary'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
// Resource type the report is about. Plural noun, e.g. databases, sites, urls.
'$id' => ID::custom('targetType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
// Free-form target identifier (URL for lighthouse, resource ID for db).
// Indexed by `_key_project_target` with an explicit prefix length.
'$id' => ID::custom('target'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
// Category strings, e.g. 'performance', 'accessibility'. Native array
// column — we never query on individual entries (MySQL JSON-array
// indexes are weak), this is read+rewrite only.
'$id' => ID::custom('categories'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => [],
],
[
// Virtual attribute — insights live in the `insights` collection
// back-referenced by `reportInternalId`. The subQuery filter joins
// them at read time.
'$id' => ID::custom('insights'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['subQueryReportInsights'],
],
[
'$id' => ID::custom('analyzedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_project_app_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'appInternalId', 'type'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_target'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'appInternalId', 'targetType', 'target'],
'lengths' => [null, null, null, 700],
'orders' => [],
],
],
],
'insights' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('insights'),
'name' => 'Insights',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('reportInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('reportId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('severity'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16,
'signed' => true,
'required' => true,
'default' => 'active',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('parentResourceType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('parentResourceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('parentResourceInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('title'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('summary'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('ctas'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('analyzedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('dismissedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('dismissedBy'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_project_report'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'reportInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_resource'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'resourceType', 'resourceId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_parent_resource'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'parentResourceType', 'parentResourceId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'type'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_severity'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'severity'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'status'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_dismissedAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'dismissedAt'],
'lengths' => [],
'orders' => [Database::ORDER_ASC, Database::ORDER_DESC],
],
],
],
];
// Organization API keys subquery
-186
View File
@@ -841,28 +841,6 @@ return [
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('providerBranches'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('providerPaths'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
],
'indexes' => [
[
@@ -1342,28 +1320,6 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('providerBranches'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('providerPaths'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
],
'indexes' => [
[
@@ -2798,146 +2754,4 @@ return [
],
],
],
// Naming it presenceLogs as later it might be only be used as a presence events table only and not for the actual presence
'presenceLogs' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('presenceLogs'),
'name' => 'Presence Logs',
'attributes' => [
[
'$id' => ID::custom('userInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('expiresAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('source'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('hostname'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('metadata'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => new \stdClass(),
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('permissionsHash'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 32,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_unique_userId'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['userId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_userInternal'),
'type' => Database::INDEX_KEY,
'attributes' => ['userInternalId'],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_expiresAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['expiresAt'],
'lengths' => [],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['status'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_source'),
'type' => Database::INDEX_KEY,
'attributes' => ['source'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_source_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['source', 'status']
],
[
'$id' => ID::custom('_key_permissionsHash'),
'type' => Database::INDEX_KEY,
'attributes' => ['permissionsHash']
]
]
]
];
-1
View File
@@ -22,7 +22,6 @@ return [
'X-Appwrite-Locale',
'X-Appwrite-Mode',
'X-Appwrite-JWT',
'X-Appwrite-Organization',
'X-Appwrite-Response-Format',
'X-Appwrite-Timeout',
'X-Appwrite-ID',
-66
View File
@@ -623,11 +623,6 @@ return [
'description' => 'Synchronous function execution timed out. Use asynchronous execution instead, or ensure the execution duration doesn\'t exceed 30 seconds.',
'code' => 408,
],
Exception::FUNCTION_ASYNCHRONOUS_TIMEOUT => [
'name' => Exception::FUNCTION_ASYNCHRONOUS_TIMEOUT,
'description' => 'Asynchronous function execution timed out. Ensure the execution duration doesn\'t exceed the configured function timeout.',
'code' => 408,
],
Exception::FUNCTION_TEMPLATE_NOT_FOUND => [
'name' => Exception::FUNCTION_TEMPLATE_NOT_FOUND,
'description' => 'Function Template with the requested ID could not be found.',
@@ -692,11 +687,6 @@ return [
'description' => 'Build with the requested ID failed. Please check the logs for more information.',
'code' => 400,
],
Exception::BUILD_TIMEOUT => [
'name' => Exception::BUILD_TIMEOUT,
'description' => 'Build timed out. Increase the build timeout via the `_APP_COMPUTE_BUILD_TIMEOUT` environment variable, or simplify the build to complete within the limit.',
'code' => 408,
],
/** Deployments */
Exception::DEPLOYMENT_NOT_FOUND => [
@@ -725,18 +715,6 @@ return [
'code' => 404,
],
/** Presence */
Exception::PRESENCE_NOT_FOUND => [
'name' => Exception::PRESENCE_NOT_FOUND,
'description' => 'Presence with the requested ID could not be found.',
'code' => 404,
],
Exception::PRESENCE_ALREADY_EXISTS => [
'name' => Exception::PRESENCE_ALREADY_EXISTS,
'description' => 'Presence with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
/** Databases */
Exception::DATABASE_NOT_FOUND => [
'name' => Exception::DATABASE_NOT_FOUND,
@@ -1258,26 +1236,6 @@ return [
'description' => 'The specified database type is not supported for CSV import or export operations.',
'code' => 400,
],
Exception::MIGRATION_SOURCE_PROJECT_ID_REQUIRED => [
'name' => Exception::MIGRATION_SOURCE_PROJECT_ID_REQUIRED,
'description' => 'A source projectId is required for Appwrite migrations. Provide it in the migration credentials.',
'code' => 400,
],
Exception::MIGRATION_SOURCE_PROJECT_NOT_FOUND => [
'name' => Exception::MIGRATION_SOURCE_PROJECT_NOT_FOUND,
'description' => 'The source project for the provided projectId was not found. Verify the projectId and the API key has access to it.',
'code' => 404,
],
Exception::MIGRATION_SOURCE_TYPE_INVALID => [
'name' => Exception::MIGRATION_SOURCE_TYPE_INVALID,
'description' => 'The migration source type is invalid. Use one of the supported source types.',
'code' => 400,
],
Exception::MIGRATION_DESTINATION_TYPE_INVALID => [
'name' => Exception::MIGRATION_DESTINATION_TYPE_INVALID,
'description' => 'The migration destination type is invalid. Use one of the supported destination types.',
'code' => 400,
],
/** Realtime */
Exception::REALTIME_MESSAGE_FORMAT_INVALID => [
@@ -1465,28 +1423,4 @@ return [
'description' => 'The maximum number of mock phones for this project has been reached.',
'code' => 400,
],
/** Advisor */
Exception::INSIGHT_NOT_FOUND => [
'name' => Exception::INSIGHT_NOT_FOUND,
'description' => 'Insight with the requested ID could not be found.',
'code' => 404,
],
Exception::INSIGHT_ALREADY_EXISTS => [
'name' => Exception::INSIGHT_ALREADY_EXISTS,
'description' => 'Insight with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
/** Reports */
Exception::REPORT_NOT_FOUND => [
'name' => Exception::REPORT_NOT_FOUND,
'description' => 'Report with the requested ID could not be found.',
'code' => 404,
],
Exception::REPORT_ALREADY_EXISTS => [
'name' => Exception::REPORT_ALREADY_EXISTS,
'description' => 'Report with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
];
+1 -29
View File
@@ -426,33 +426,5 @@ return [
'update' => [
'$description' => 'This event triggers when a proxy rule is updated.',
]
],
'reports' => [
'$model' => Response::MODEL_REPORT,
'$resource' => true,
'$description' => 'This event triggers on any report event.',
'create' => [
'$description' => 'This event triggers when a report is created.',
],
'update' => [
'$description' => 'This event triggers when a report is updated.',
],
'delete' => [
'$description' => 'This event triggers when a report is deleted.',
],
'insights' => [
'$model' => Response::MODEL_INSIGHT,
'$resource' => true,
'$description' => 'This event triggers on any insight event.',
'create' => [
'$description' => 'This event triggers when an insight is created.',
],
'update' => [
'$description' => 'This event triggers when an insight is updated.',
],
'delete' => [
'$description' => 'This event triggers when an insight is deleted.',
],
],
],
]
];
+14 -70
View File
@@ -14,11 +14,7 @@ return [
'name' => 'Analog',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/analog/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/analog/env.sh',
'adapters' => [
@@ -44,11 +40,7 @@ return [
'name' => 'Angular',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/angular/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/angular/env.sh',
'adapters' => [
@@ -74,11 +66,7 @@ return [
'name' => 'Next.js',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/next-js/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/next-js/env.sh',
'adapters' => [
@@ -103,11 +91,7 @@ return [
'name' => 'React',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -124,11 +108,7 @@ return [
'name' => 'Nuxt',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/nuxt/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/nuxt/env.sh',
'adapters' => [
@@ -153,11 +133,7 @@ return [
'name' => 'Vue.js',
'screenshotSleep' => 5000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -174,11 +150,7 @@ return [
'name' => 'SvelteKit',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/sveltekit/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/sveltekit/env.sh',
'adapters' => [
@@ -203,11 +175,7 @@ return [
'name' => 'Astro',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/astro/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/astro/env.sh',
'adapters' => [
@@ -232,11 +200,7 @@ return [
'name' => 'TanStack Start',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/tanstack-start/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/tanstack-start/env.sh',
'adapters' => [
@@ -261,11 +225,7 @@ return [
'name' => 'Remix',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/remix/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/remix/env.sh',
'adapters' => [
@@ -290,11 +250,7 @@ return [
'name' => 'Lynx',
'screenshotSleep' => 5000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -328,11 +284,7 @@ return [
'name' => 'React Native',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -349,11 +301,7 @@ return [
'name' => 'Vite',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -369,11 +317,7 @@ return [
'name' => 'Other',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
+3 -11
View File
@@ -12,8 +12,6 @@ $member = [
'account',
'teams.read',
'teams.write',
'presences.read',
'presences.write',
'documents.read',
'documents.write',
'rows.read',
@@ -49,8 +47,6 @@ $admins = [
'buckets.write',
'users.read',
'users.write',
'presences.read',
'presences.write',
'databases.read',
'databases.write',
'collections.read',
@@ -63,8 +59,8 @@ $admins = [
'oauth2.write',
'mocks.read',
'mocks.write',
'project.policies.read',
'project.policies.write',
'policies.read',
'policies.write',
'templates.read',
'templates.write',
'projects.write',
@@ -107,10 +103,6 @@ $admins = [
'tokens.write',
'schedules.read',
'schedules.write',
'insights.read',
'insights.write',
'reports.read',
'reports.write',
];
return [
@@ -150,7 +142,7 @@ return [
'label' => 'Owner',
'scopes' => \array_merge($member, $admins),
],
User::ROLE_KEYS => [
User::ROLE_APPS => [
'label' => 'Applications',
'scopes' => ['global', 'health.read', 'graphql'],
],
+2 -8
View File
@@ -4,23 +4,17 @@
return [
"projects.read" => [
"description" => 'Access to read organization projects',
"category" => "Projects",
"description" => 'Access to read organization\'s projects',
],
"projects.write" => [
"description" =>
"Access to create, update, and delete organization projects",
"category" => "Projects",
"Access to create, update, and delete projects in organization",
],
"devKeys.read" => [
"description" => 'Access to read project\'s development keys',
"category" => "Other",
"deprecated" => true,
],
"devKeys.write" => [
"description" =>
"Access to create, update, and delete project\'s development keys",
"category" => "Other",
"deprecated" => true,
],
];
+7 -57
View File
@@ -44,23 +44,11 @@ return [
"category" => "Project",
],
"policies.read" => [
"description" =>
"Access to read project\'s policies. Replaced by \'project.policies.read\' for more granular control",
"category" => "Project",
'deprecated' => true,
],
"policies.write" => [
"description" =>
"Access to update project\'s policies. Replaces by \'project.policies.write\' for more granular control",
"category" => "Project",
'deprecated' => true,
],
"project.policies.read" => [
"description" =>
"Access to read project\'s policies",
"category" => "Project",
],
"project.policies.write" => [
"policies.write" => [
"description" =>
"Access to update project\'s policies",
"category" => "Project",
@@ -227,16 +215,6 @@ return [
'description' => 'Access to create function executions',
'category' => 'Functions',
],
'execution.read' => [
'description' => 'Access to read function executions. This scope is deprecated for consistency purposes, and replaced by `executions.read`.',
'category' => 'Functions',
'deprecated' => true,
],
'execution.write' => [
'description' => 'Access to create function executions. This scope is deprecated for consistency purposes, and replaced by `executions.write`.',
'category' => 'Functions',
'deprecated' => true,
],
// Sites
'sites.read' => [
@@ -298,16 +276,6 @@ return [
'category' => 'Messaging',
],
// Proxy
'rules.read' => [
'description' => 'Access to read proxy rules.',
'category' => 'Proxy',
],
'rules.write' => [
'description' => 'Access to create, update, and delete proxy rules.',
'category' => 'Proxy',
],
// Other
"webhooks.read" => [
"description" =>
@@ -361,30 +329,12 @@ return [
'description' => 'Access to create, update, and delete resources under VCS service.',
'category' => 'Other',
],
// Advisor
'insights.read' => [
'description' => 'Access to read insights under Advisor service.',
'category' => 'Advisor',
'rules.read' => [
'description' => 'Access to read proxy rules.',
'category' => 'Other',
],
'insights.write' => [
'description' => 'Reserved for Advisor insight ingestion outside CE.',
'category' => 'Advisor',
],
'reports.read' => [
'description' => 'Access to read reports under Advisor service.',
'category' => 'Advisor',
],
'reports.write' => [
'description' => 'Access to delete reports under Advisor service.',
'category' => 'Advisor',
],
'presences.read' => [
'description' => 'Access to read your project\'s presences',
'category' => 'Presences',
],
'presences.write' => [
'description' => 'Access to create, update, and delete your project\'s presences',
'category' => 'Presences',
'rules.write' => [
'description' => 'Access to create, update, and delete proxy rules.',
'category' => 'Other',
],
];
-20
View File
@@ -320,26 +320,6 @@ return [
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/claude-plugin/CHANGELOG.md'),
],
[
'key' => 'codex-plugin',
'name' => 'CodexPlugin',
'version' => '0.1.1',
'url' => 'https://github.com/appwrite/codex-plugin.git',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'spec' => 'static',
'family' => APP_SDK_PLATFORM_STATIC,
'prism' => 'codex-plugin',
'source' => \realpath(__DIR__ . '/../sdks/static-codex-plugin'),
'gitUrl' => 'git@github.com:appwrite/codex-plugin.git',
'gitRepoName' => 'codex-plugin',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/codex-plugin/CHANGELOG.md'),
],
],
],
+1 -15
View File
@@ -308,19 +308,5 @@ return [
'optional' => true,
'icon' => '/images/services/messaging.png',
'platforms' => ['client', 'server', 'console'],
],
'advisor' => [
'key' => 'advisor',
'name' => 'Advisor',
'subtitle' => 'The Advisor service surfaces actionable reports about your project resources, with CTA descriptors for one-click remediation in the console.',
'description' => '/docs/services/advisor.md',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/server/advisor',
'tests' => true,
'optional' => true,
'icon' => '/images/services/insights.png',
'platforms' => ['server', 'console'],
],
]
];
+2 -3
View File
@@ -79,13 +79,12 @@ return [
...getRuntimes($templateRuntimes['DENO'], 'deno cache src/main.ts', 'src/main.ts', 'deno/starter', $allowList),
...getRuntimes($templateRuntimes['BUN'], 'bun install', 'src/main.ts', 'bun/starter', $allowList),
...getRuntimes($templateRuntimes['RUBY'], 'bundle install', 'lib/main.rb', 'ruby/starter', $allowList),
...getRuntimes($templateRuntimes['RUST'], '', 'main.rs', 'rust/starter', $allowList),
],
'instructions' => 'For documentation and instructions check out the <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates">templates repository</a>.',
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/starter">file</a>.',
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.2.*',
'variables' => [],
'scopes' => ['users.read']
],
-9
View File
@@ -1336,15 +1336,6 @@ return [
'category' => 'Migrations',
'description' => '',
'variables' => [
[
'name' => '_APP_MIGRATION_HOST',
'description' => 'Internal hostname the migrations worker uses to reach this instance\'s API (for migrations and CSV/JSON imports & exports). Defaults to \'appwrite\', the API service name in the standard Docker Compose setup. Only change this for non-standard deployments.',
'introduction' => '1.9.0',
'default' => 'appwrite',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_MIGRATIONS_FIREBASE_CLIENT_ID',
'description' => 'Google OAuth client ID. You can find it in your GCP application settings.',
+170 -179
View File
@@ -11,13 +11,10 @@ use Appwrite\Auth\Validator\PersonalData;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Bus\Events\SessionCreated;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Redirect;
@@ -67,7 +64,6 @@ use Utopia\Emails\Email;
use Utopia\Emails\Validator\Email as EmailValidator;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\Platform\Enum;
use Utopia\Storage\Validator\FileName;
use Utopia\System\System;
use Utopia\Validator;
@@ -336,15 +332,15 @@ Http::post('/v1/account')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -474,9 +470,9 @@ Http::delete('/v1/account')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('publisherForDeletes')
->inject('queueForDeletes')
->inject('authorization')
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, DeletePublisher $publisherForDeletes, Authorization $authorization) {
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes, Authorization $authorization) {
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
@@ -500,11 +496,9 @@ Http::delete('/v1/account')
$dbForProject->deleteDocument('users', $user->getId());
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_DOCUMENT,
document: $user,
));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($user);
$queueForEvents
->setParam('userId', $user->getId())
@@ -586,12 +580,12 @@ Http::delete('/v1/account/sessions')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('publisherForDeletes')
->inject('queueForDeletes')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, DeletePublisher $publisherForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
@@ -621,11 +615,10 @@ Http::delete('/v1/account/sessions')
$queueForEvents
->setPayload($response->output($session, Response::MODEL_SESSION));
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_SESSION_TARGETS,
document: $session,
));
$queueForDeletes
->setType(DELETE_TYPE_SESSION_TARGETS)
->setDocument($session)
->trigger();
}
}
@@ -719,12 +712,12 @@ Http::delete('/v1/account/sessions/:sessionId')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('publisherForDeletes')
->inject('queueForDeletes')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, DeletePublisher $publisherForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
@@ -766,11 +759,10 @@ Http::delete('/v1/account/sessions/:sessionId')
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION));
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_SESSION_TARGETS,
document: $session,
));
$queueForDeletes
->setType(DELETE_TYPE_SESSION_TARGETS)
->setDocument($session)
->trigger();
$response->noContent();
return;
@@ -838,11 +830,11 @@ Http::patch('/v1/account/sessions/:sessionId')
$refreshToken = $session->getAttribute('providerRefreshToken', '');
$oAuthProviders = Config::getParam('oAuthProviders') ?? [];
$className = $oAuthProviders[$provider]['class'] ?? null;
if (!empty($refreshToken) && ($className === null || !\class_exists($className))) {
if (!empty($provider) && ($className === null || !\class_exists($className))) {
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
if ($className !== null && \class_exists($className)) {
if (!empty($provider) && \class_exists($className)) {
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
@@ -1244,7 +1236,7 @@ Http::get('/v1/account/sessions/oauth2/:provider')
))
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.', enum: new Enum(name: 'OAuthProvider', exclude: ['mock', 'mock-unverified']))
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.')
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
@@ -1684,15 +1676,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
@@ -1825,15 +1817,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
@@ -2010,7 +2002,7 @@ Http::get('/v1/account/tokens/oauth2/:provider')
))
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.', enum: new Enum(name: 'OAuthProvider', exclude: ['mock', 'mock-unverified']))
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.')
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
@@ -2121,12 +2113,12 @@ Http::post('/v1/account/tokens/magic-url')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('publisherForMails')
->inject('queueForMails')
->inject('plan')
->inject('proofForPassword')
->inject('platform')
->inject('authorization')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2183,15 +2175,15 @@ Http::post('/v1/account/tokens/magic-url')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2312,7 +2304,6 @@ Http::post('/v1/account/tokens/magic-url')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2330,6 +2321,13 @@ Http::post('/v1/account/tokens/magic-url')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -2350,17 +2348,11 @@ Http::post('/v1/account/tokens/magic-url')
$subject = $customTemplate['subject'] ?? $subject;
}
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
$projectName = $project->getAttribute('name');
@@ -2382,17 +2374,18 @@ Http::post('/v1/account/tokens/magic-url')
'team' => '',
];
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $email,
subject: $subject,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->appendVariables($emailVariables)
->setRecipient($email);
if ($project->getId() === 'console') {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$token->setAttribute('secret', $tokenSecret);
@@ -2443,12 +2436,12 @@ Http::post('/v1/account/tokens/email')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('publisherForMails')
->inject('queueForMails')
->inject('plan')
->inject('proofForPassword')
->inject('proofForCode')
->inject('authorization')
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2503,15 +2496,15 @@ Http::post('/v1/account/tokens/email')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2640,7 +2633,6 @@ Http::post('/v1/account/tokens/email')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2658,6 +2650,13 @@ Http::post('/v1/account/tokens/email')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -2678,17 +2677,11 @@ Http::post('/v1/account/tokens/email')
$subject = $customTemplate['subject'] ?? $subject;
}
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
$projectName = $project->getAttribute('name');
@@ -2724,18 +2717,20 @@ Http::post('/v1/account/tokens/email')
]);
}
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $email,
subject: $subject,
bodyTemplate: $bodyTemplate,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setBodyTemplate($bodyTemplate)
->appendVariables($emailVariables)
->setRecipient($email);
// since this is console project, set email sender name!
if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$token->setAttribute('secret', $tokenSecret);
@@ -2885,7 +2880,7 @@ Http::post('/v1/account/tokens/phone')
->inject('platform')
->inject('dbForProject')
->inject('queueForEvents')
->inject('publisherForMessaging')
->inject('queueForMessaging')
->inject('locale')
->inject('timelimit')
->inject('usage')
@@ -2893,7 +2888,7 @@ Http::post('/v1/account/tokens/phone')
->inject('store')
->inject('proofForCode')
->inject('authorization')
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, MessagingPublisher $publisherForMessaging, Locale $locale, callable $timelimit, Context $usage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, Context $usage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -3026,13 +3021,11 @@ Http::post('/v1/account/tokens/phone')
],
]);
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_INTERNAL,
project: $project,
message: $messageDoc,
recipients: [$phone],
providerType: MESSAGE_TYPE_SMS,
));
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_INTERNAL)
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType(MESSAGE_TYPE_SMS);
$helper = PhoneNumberUtil::getInstance();
try {
@@ -3424,15 +3417,15 @@ Http::patch('/v1/account/email')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -3688,11 +3681,11 @@ Http::post('/v1/account/recovery')
->inject('project')
->inject('platform')
->inject('locale')
->inject('publisherForMails')
->inject('queueForMails')
->inject('queueForEvents')
->inject('proofForToken')
->inject('authorization')
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, MailPublisher $publisherForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -3775,7 +3768,6 @@ Http::post('/v1/account/recovery')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -3793,6 +3785,13 @@ Http::post('/v1/account/recovery')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -3813,17 +3812,11 @@ Http::post('/v1/account/recovery')
$subject = $customTemplate['subject'] ?? $subject;
}
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
$emailVariables = [
@@ -3836,18 +3829,19 @@ Http::post('/v1/account/recovery')
'team' => ''
];
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $profile->getAttribute('email', ''),
name: $profile->getAttribute('name', ''),
subject: $subject,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$queueForMails
->setRecipient($profile->getAttribute('email', ''))
->setName($profile->getAttribute('name', ''))
->setBody($body)
->appendVariables($emailVariables)
->setSubject($subject)
->setPreview($preview);
if ($project->getId() === 'console') {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$recovery->setAttribute('secret', $secret);
@@ -4015,10 +4009,10 @@ Http::post('/v1/account/verifications/email')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('publisherForMails')
->inject('queueForMails')
->inject('proofForToken')
->inject('authorization')
->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, ProofsToken $proofForToken, Authorization $authorization) {
->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -4105,7 +4099,6 @@ Http::post('/v1/account/verifications/email')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -4123,6 +4116,13 @@ Http::post('/v1/account/verifications/email')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -4143,17 +4143,11 @@ Http::post('/v1/account/verifications/email')
$subject = $customTemplate['subject'] ?? $subject;
}
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
$emailVariables = [
@@ -4180,19 +4174,20 @@ Http::post('/v1/account/verifications/email')
]);
}
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $user->getAttribute('email'),
name: $user->getAttribute('name') ?? '',
subject: $subject,
bodyTemplate: $bodyTemplate,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setBodyTemplate($bodyTemplate)
->appendVariables($emailVariables)
->setRecipient($user->getAttribute('email'))
->setName($user->getAttribute('name') ?? '');
if ($project->getId() === 'console') {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$verification->setAttribute('secret', $verificationSecret);
@@ -4326,7 +4321,7 @@ Http::post('/v1/account/verifications/phone')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
->inject('publisherForMessaging')
->inject('queueForMessaging')
->inject('project')
->inject('locale')
->inject('timelimit')
@@ -4334,7 +4329,7 @@ Http::post('/v1/account/verifications/phone')
->inject('plan')
->inject('proofForCode')
->inject('authorization')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, MessagingPublisher $publisherForMessaging, Document $project, Locale $locale, callable $timelimit, Context $usage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, Context $usage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -4403,13 +4398,11 @@ Http::post('/v1/account/verifications/phone')
],
]);
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_INTERNAL,
project: $project,
message: $messageDoc,
recipients: [$user->getAttribute('phone')],
providerType: MESSAGE_TYPE_SMS,
));
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_INTERNAL)
->setMessage($messageDoc)
->setRecipients([$user->getAttribute('phone')])
->setProviderType(MESSAGE_TYPE_SMS);
$helper = PhoneNumberUtil::getInstance();
try {
@@ -4681,13 +4674,13 @@ Http::delete('/v1/account/targets/:targetId/push')
))
->param('targetId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID.', false, ['dbForProject'])
->inject('queueForEvents')
->inject('publisherForDeletes')
->inject('queueForDeletes')
->inject('user')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('authorization')
->action(function (string $targetId, Event $queueForEvents, DeletePublisher $publisherForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
if ($target->isEmpty()) {
@@ -4702,11 +4695,9 @@ Http::delete('/v1/account/targets/:targetId/push')
$dbForProject->purgeCachedDocument('users', $user->getId());
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_TARGET,
document: $target,
));
$queueForDeletes
->setType(DELETE_TYPE_TARGET)
->setDocument($target);
$queueForEvents
->setParam('userId', $user->getId())
+1 -1
View File
@@ -39,7 +39,7 @@ Http::init()
if (
array_key_exists('graphql', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['graphql']
&& !($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles()))
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
+41 -58
View File
@@ -3,11 +3,9 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Event\Messaging;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Status as MessageStatus;
use Appwrite\Permission;
@@ -47,7 +45,6 @@ use Utopia\Database\Validator\UID;
use Utopia\Emails\Validator\Email;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\Platform\Enum;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
@@ -365,7 +362,7 @@ Http::post('/v1/messaging/providers/smtp')
->param('port', 587, new Range(1, 65535), 'The default SMTP server port.', true)
->param('username', '', new Text(0), 'Authentication username.', true)
->param('password', '', new Text(0), 'Authentication password.', true)
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be omitted, \'ssl\', or \'tls\'', true, enum: new Enum(name: 'SmtpEncryption'))
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be omitted, \'ssl\', or \'tls\'', true)
->param('autoTLS', true, new Boolean(), 'Enable SMTP AutoTLS feature.', true)
->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true)
->param('fromName', '', new Text(128, 0), 'Sender Name.', true)
@@ -1603,7 +1600,7 @@ Http::patch('/v1/messaging/providers/smtp/:providerId')
->param('port', null, new Nullable(new Range(1, 65535)), 'SMTP port.', true)
->param('username', '', new Text(0), 'Authentication username.', true)
->param('password', '', new Text(0), 'Authentication password.', true)
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true, enum: new Enum(name: 'SmtpEncryption'))
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true)
->param('autoTLS', null, new Nullable(new Boolean()), 'Enable SMTP AutoTLS feature.', true)
->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
@@ -2730,9 +2727,9 @@ Http::delete('/v1/messaging/topics/:topicId')
->param('topicId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Topic ID.', false, ['dbForProject'])
->inject('queueForEvents')
->inject('dbForProject')
->inject('publisherForDeletes')
->inject('queueForDeletes')
->inject('response')
->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, DeletePublisher $publisherForDeletes, Response $response) {
->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, Delete $queueForDeletes, Response $response) {
$topic = $dbForProject->getDocument('topics', $topicId);
if ($topic->isEmpty()) {
@@ -2741,11 +2738,9 @@ Http::delete('/v1/messaging/topics/:topicId')
$dbForProject->deleteDocument('topics', $topicId);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_TOPIC,
document: $topic,
));
$queueForDeletes
->setType(DELETE_TYPE_TOPIC)
->setDocument($topic);
$queueForEvents
->setParam('topicId', $topic->getId());
@@ -3192,9 +3187,9 @@ Http::post('/v1/messaging/messages/email')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('publisherForMessaging')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3279,11 +3274,9 @@ Http::post('/v1/messaging/messages/email')
switch ($status) {
case MessageStatus::PROCESSING:
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3369,9 +3362,9 @@ Http::post('/v1/messaging/messages/sms')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('publisherForMessaging')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3425,11 +3418,9 @@ Http::post('/v1/messaging/messages/sms')
switch ($status) {
case MessageStatus::PROCESSING:
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3502,15 +3493,15 @@ Http::post('/v1/messaging/messages/push')
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', false, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', false, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', 'high', new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device state and may not deliver notifications immediately. "high" will always attempt to immediately deliver the notification.', true, enum: new Enum(name: 'MessagePriority'))
->param('priority', 'high', new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device state and may not deliver notifications immediately. "high" will always attempt to immediately deliver the notification.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('publisherForMessaging')
->inject('queueForMessaging')
->inject('response')
->inject('platform')
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response, array $platform) {
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3647,11 +3638,9 @@ Http::post('/v1/messaging/messages/push')
switch ($status) {
case MessageStatus::PROCESSING:
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3994,9 +3983,9 @@ Http::patch('/v1/messaging/messages/email/:messageId')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('publisherForMessaging')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4152,11 +4141,9 @@ Http::patch('/v1/messaging/messages/email/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
}
$queueForEvents
@@ -4218,9 +4205,9 @@ Http::patch('/v1/messaging/messages/sms/:messageId')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('publisherForMessaging')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4336,11 +4323,9 @@ Http::patch('/v1/messaging/messages/sms/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
}
$queueForEvents
@@ -4389,15 +4374,15 @@ Http::patch('/v1/messaging/messages/push/:messageId')
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', null, new Nullable(new Boolean()), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', null, new Nullable(new Boolean()), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', null, new Nullable(new WhiteList(['normal', 'high'])), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true, enum: new Enum(name: 'MessagePriority'))
->param('priority', null, new Nullable(new WhiteList(['normal', 'high'])), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('publisherForMessaging')
->inject('queueForMessaging')
->inject('response')
->inject('platform')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response, array $platform) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4599,11 +4584,9 @@ Http::patch('/v1/messaging/messages/push/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
}
$queueForEvents
+1 -8
View File
@@ -10,7 +10,6 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DateTimeValidator;
use Utopia\Http\Http;
use Utopia\Platform\Enum;
use Utopia\Validator\WhiteList;
Http::get('/v1/project/usage')
@@ -32,13 +31,7 @@ Http::get('/v1/project/usage')
))
->param('startDate', '', new DateTimeValidator(), 'Starting date for the usage')
->param('endDate', '', new DateTimeValidator(), 'End date for the usage')
->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true, enum: new Enum(
name: 'ProjectUsageRange',
map: [
'1h' => 'OneHour',
'1d' => 'OneDay',
]
))
->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true)
->inject('response')
->inject('project')
->inject('dbForProject')
+34
View File
@@ -2,6 +2,9 @@
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
@@ -24,6 +27,37 @@ Http::init()
}
});
Http::get('/v1/projects/:projectId')
->desc('Get project')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'get',
description: '/docs/references/projects/get.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$response->dynamic($project, Response::MODEL_PROJECT);
});
// Backwards compatibility
Http::patch('/v1/projects/:projectId/oauth2')
->desc('Update project OAuth2')
+22 -60
View File
@@ -11,9 +11,8 @@ use Appwrite\Auth\Validator\Phone;
use Appwrite\Deletes\Identities as DeleteIdentities;
use Appwrite\Deletes\Targets as DeleteTargets;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\SDK\AuthType;
@@ -63,7 +62,6 @@ use Utopia\Emails\Email;
use Utopia\Emails\Validator\Email as EmailValidator;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\Platform\Enum;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
@@ -133,15 +131,15 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
} catch (\Throwable) {
}
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -433,7 +431,7 @@ Http::post('/v1/users/sha')
->param('userId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using SHA.')
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true, enum: new Enum(name: 'PasswordHash'))
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('project')
@@ -607,7 +605,7 @@ Http::post('/v1/users/:userId/targets')
))
->param('targetId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
->param('providerType', '', new WhiteList([MESSAGE_TYPE_EMAIL, MESSAGE_TYPE_SMS, MESSAGE_TYPE_PUSH]), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.', enum: new Enum(name: 'MessagingProviderType'))
->param('providerType', '', new WhiteList([MESSAGE_TYPE_EMAIL, MESSAGE_TYPE_SMS, MESSAGE_TYPE_PUSH]), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.')
->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
->param('providerId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true, ['dbForProject'])
->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true)
@@ -733,13 +731,6 @@ Http::get('/v1/users')
$cursor->setValue($cursorDocument);
}
$skipFilters = ['subQueryAuthenticators', 'subQuerySessions', 'subQueryTokens', 'subQueryChallenges', 'subQueryMemberships'];
$selects = Query::getByType($queries, [Query::TYPE_SELECT]);
if (empty($selects)) {
$skipFilters[] = 'subQueryTargets';
}
$users = [];
$total = 0;
@@ -752,32 +743,7 @@ Http::get('/v1/users')
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
}, $skipFilters);
if (empty($selects) && !empty($users)) {
$sequences = [];
foreach ($users as $user) {
$sequences[] = $user->getSequence();
}
try {
$targets = $dbForProject->getAuthorization()->skip(fn () => $dbForProject->find('targets', [
Query::equal('userInternalId', $sequences),
Query::limit(PHP_INT_MAX),
]));
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$targetsByUser = [];
foreach ($targets as $target) {
$targetsByUser[$target->getAttribute('userInternalId')][] = $target;
}
foreach ($users as $user) {
$user->setAttribute('targets', $targetsByUser[$user->getSequence()] ?? []);
}
}
}, ['subQueryAuthenticators', 'subQuerySessions', 'subQueryTokens', 'subQueryChallenges', 'subQueryMemberships']);
$response->dynamic(new Document([
'users' => $users,
@@ -1597,15 +1563,15 @@ Http::patch('/v1/users/:userId/email')
} catch (\Throwable) {
}
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2319,7 +2285,7 @@ Http::delete('/v1/users/:userId/mfa/authenticators/:type')
)
])
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.', enum: new Enum(name: 'AuthenticatorType'))
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -2626,8 +2592,8 @@ Http::delete('/v1/users/:userId')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('publisherForDeletes')
->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents, DeletePublisher $publisherForDeletes) {
->inject('queueForDeletes')
->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
$user = $dbForProject->getDocument('users', $userId);
@@ -2642,11 +2608,9 @@ Http::delete('/v1/users/:userId')
DeleteIdentities::delete($dbForProject, Query::equal('userInternalId', [$user->getSequence()]));
DeleteTargets::delete($dbForProject, Query::equal('userInternalId', [$user->getSequence()]));
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_DOCUMENT,
document: $clone,
));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($clone);
$queueForEvents
->setParam('userId', $user->getId())
@@ -2679,10 +2643,10 @@ Http::delete('/v1/users/:userId/targets/:targetId')
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
->param('targetId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID.', false, ['dbForProject'])
->inject('queueForEvents')
->inject('publisherForDeletes')
->inject('queueForDeletes')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, Event $queueForEvents, DeletePublisher $publisherForDeletes, Response $response, Database $dbForProject) {
->action(function (string $userId, string $targetId, Event $queueForEvents, Delete $queueForDeletes, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
@@ -2702,11 +2666,9 @@ Http::delete('/v1/users/:userId/targets/:targetId')
$dbForProject->deleteDocument('targets', $target->getId());
$dbForProject->purgeCachedDocument('users', $user->getId());
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_TARGET,
document: $target,
));
$queueForDeletes
->setType(DELETE_TYPE_TARGET)
->setDocument($target);
$queueForEvents
->setParam('userId', $user->getId())
@@ -2832,7 +2794,7 @@ Http::get('/v1/users/usage')
)
]
))
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum(name: 'UsageRange'))
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->inject('authorization')
+75 -74
View File
@@ -7,10 +7,9 @@ use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
@@ -29,8 +28,6 @@ use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
use Appwrite\Utopia\Request\Filters\V24 as RequestV24;
use Appwrite\Utopia\Request\Filters\V25 as RequestV25;
use Appwrite\Utopia\Request\Filters\V26 as RequestV26;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -41,10 +38,7 @@ use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
use Appwrite\Utopia\Response\Filters\V24 as ResponseV24;
use Appwrite\Utopia\Response\Filters\V25 as ResponseV25;
use Appwrite\Utopia\Response\Filters\V26 as ResponseV26;
use Appwrite\Utopia\View;
use Executor\Exception\Timeout as ExecutorTimeout;
use Executor\Executor;
use MaxMind\Db\Reader;
use Swoole\Http\Request as SwooleRequest;
@@ -75,7 +69,7 @@ use Utopia\Validator\Text;
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeletePublisher $publisherForDeletes, int $executionsRetentionCount)
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname();
if (!empty($previewHostname)) {
@@ -184,7 +178,31 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
if (!empty($rule->getAttribute('deploymentId', ''))) {
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
} else {
// 1.6.x DB schema compatibility
// TODO: Make sure deploymentId is never empty, and remove this code
// Check if site or function; should never be site, but better safe than sorry
// Attempts to use attribute from both schemas (1.6 and 1.7)
$resourceType = $rule->getAttribute('deploymentResourceType', $rule->getAttribute('resourceType', ''));
// ID of site or function
$resourceId = $rule->getAttribute('deploymentResourceId', '');
// Document of site or function
$resource = $resourceType === 'function' ?
$authorization->skip(fn () => $dbForProject->getDocument('functions', $resourceId)) :
$authorization->skip(fn () => $dbForProject->getDocument('sites', $resourceId));
// ID of active deployments
// Attempts to use attribute from both schemas (1.6 and 1.7)
$activeDeploymentId = $resource->getAttribute('deploymentId', $resource->getAttribute('deployment', ''));
// Get deployment document, as intended originally
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
}
if ($deployment->isEmpty()) {
$resourceType = $rule->getAttribute('deploymentResourceType', '');
@@ -561,30 +579,26 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
'site' => '',
};
try {
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deployment->getId(),
body: \strlen($body) > 0 ? $body : null,
variables: $vars,
timeout: $resource->getAttribute('timeout', 30),
image: $runtime['image'],
source: $source,
entrypoint: $entrypoint,
version: $version,
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $runtimeEntrypoint,
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
logging: $resource->getAttribute('logging', true),
requestTimeout: 30,
responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS
);
} catch (ExecutorTimeout $th) {
throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT, previous: $th);
}
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deployment->getId(),
body: \strlen($body) > 0 ? $body : null,
variables: $vars,
timeout: $resource->getAttribute('timeout', 30),
image: $runtime['image'],
source: $source,
entrypoint: $entrypoint,
version: $version,
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $runtimeEntrypoint,
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
logging: $resource->getAttribute('logging', true),
requestTimeout: 30,
responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS
);
$headerOverrides = [];
@@ -767,12 +781,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
? RESOURCE_TYPE_FUNCTIONS
: RESOURCE_TYPE_SITES;
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_EXECUTIONS_LIMIT,
resource: (string) $resource->getSequence(),
resourceType: $resourceType,
));
$queueForDeletes
->setProject($project)
->setResourceType($resourceType)
->setResource($resource->getSequence())
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
return true;
@@ -833,10 +847,9 @@ Http::init()
->inject('apiKey')
->inject('cors')
->inject('authorization')
->inject('publisherForDeletes')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->inject('params')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount, array $params) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
@@ -844,15 +857,15 @@ Http::init()
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->match($request)?->route->label('router', true);
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
/*
* Request format
*/
$route = $utopia->match($request)?->route;
$route = $utopia->getRoute();
$request->setRoute($route);
if ($route === null) {
@@ -877,7 +890,7 @@ Http::init()
}
if (version_compare($requestFormat, '1.8.0', '<')) {
$dbForProject = $getProjectDB($project);
$request->addFilter(new RequestV20($dbForProject, $params));
$request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request)));
}
if (version_compare($requestFormat, '1.9.0', '<')) {
$request->addFilter(new RequestV21());
@@ -891,12 +904,6 @@ Http::init()
if (version_compare($requestFormat, '1.9.3', '<')) {
$request->addFilter(new RequestV24());
}
if (version_compare($requestFormat, '1.9.4', '<')) {
$request->addFilter(new RequestV25());
}
if (version_compare($requestFormat, '1.9.5', '<')) {
$request->addFilter(new RequestV26());
}
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -921,12 +928,6 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.9.5', '<')) {
$response->addFilter(new ResponseV26());
}
if (version_compare($responseFormat, '1.9.4', '<')) {
$response->addFilter(new ResponseV25());
}
if (version_compare($responseFormat, '1.9.3', '<')) {
$response->addFilter(new ResponseV24());
}
@@ -1145,17 +1146,17 @@ Http::options()
->inject('apiKey')
->inject('cors')
->inject('authorization')
->inject('publisherForDeletes')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->match($request)?->route->label('router', true);
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1190,7 +1191,7 @@ Http::error()
->inject('authorization')
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->match($request)?->route;
$route = $utopia->getRoute();
$class = \get_class($error);
$code = $error->getCode();
$message = $error->getMessage();
@@ -1260,7 +1261,7 @@ Http::error()
if (!$publish && $project->getId() !== 'console') {
$errorUser = new DBUser();
try {
$resolvedUser = $utopia->context()->get('user');
$resolvedUser = $utopia->getResource('user');
if ($resolvedUser instanceof DBUser) {
$errorUser = $resolvedUser;
}
@@ -1279,7 +1280,7 @@ Http::error()
if ($logger && $publish) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia->context()->get('user');
$user = $utopia->getResource('user');
} catch (\Throwable) {
// All good, user is optional information for logger
}
@@ -1480,7 +1481,7 @@ Http::error()
// the cors resource (which depends on rule -> DB) would cascade.
// Uses override:true to avoid duplicate headers if init() already set them.
try {
$cors = $utopia->context()->get('cors');
$cors = $utopia->getResource('cors');
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response
->removeHeader($name)
@@ -1547,16 +1548,16 @@ Http::get('/robots.txt')
->inject('previewHostname')
->inject('apiKey')
->inject('authorization')
->inject('publisherForDeletes')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->match($request)?->route->label('router', true);
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
});
@@ -1581,16 +1582,16 @@ Http::get('/humans.txt')
->inject('previewHostname')
->inject('apiKey')
->inject('authorization')
->inject('publisherForDeletes')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->match($request)?->route->label('router', true);
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
});
+4 -3
View File
@@ -13,7 +13,6 @@ use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Http\Route;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\Text;
@@ -284,11 +283,13 @@ Http::get('/v1/mock/github/callback')
Http::shutdown()
->groups(['mock'])
->inject('utopia')
->inject('response')
->inject('route')
->action(function (Response $response, Route $route) {
->inject('request')
->action(function (Http $utopia, Response $response, Request $request) {
$result = [];
$route = $utopia->getRoute();
$path = APP_STORAGE_CACHE . '/tests.json';
$tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : [];
+149 -131
View File
@@ -3,21 +3,23 @@
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Build;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Audit as AuditMessage;
use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Storage\Config\CacheControl;
use Appwrite\Platform\Modules\Storage\Config\StorageCacheControl;
use Appwrite\SDK\Method;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
@@ -37,7 +39,6 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Roles;
use Utopia\Http\Http;
use Utopia\Http\Route;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
@@ -86,7 +87,7 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
Http::init()
->groups(['api'])
->inject('route')
->inject('utopia')
->inject('request')
->inject('dbForPlatform')
->inject('dbForProject')
@@ -99,7 +100,11 @@ Http::init()
->inject('team')
->inject('apiKey')
->inject('authorization')
->action(function (Route $route, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
/**
* Handle user authentication and session validation.
@@ -175,8 +180,8 @@ Http::init()
$role = $apiKey->getRole();
$scopes = $apiKey->getScopes();
// Handle special key role case
if ($apiKey->getRole() === User::ROLE_KEYS) {
// Handle special app role case
if ($apiKey->getRole() === User::ROLE_APPS) {
// Disable authorization checks for project API keys
// Dynamic supported for backwards compatibility
if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_EPHEMERAL || $apiKey->getType() === 'dynamic') && $apiKey->getProjectId() === $project->getId()) {
@@ -186,7 +191,7 @@ Http::init()
$user = new User([
'$id' => '',
'status' => true,
'type' => ACTOR_TYPE_KEY_PROJECT,
'type' => ACTIVITY_TYPE_KEY_PROJECT,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $apiKey->getName(),
@@ -258,9 +263,9 @@ Http::init()
$userClone = clone $user;
$userClone->setAttribute('type', match ($apiKey->getType()) {
API_KEY_STANDARD => ACTOR_TYPE_KEY_PROJECT,
API_KEY_ACCOUNT => ACTOR_TYPE_KEY_ACCOUNT,
default => ACTOR_TYPE_KEY_ORGANIZATION,
API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT,
API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT,
default => ACTIVITY_TYPE_KEY_ORGANIZATION,
});
$auditContext->user = $userClone;
}
@@ -425,7 +430,7 @@ Http::init()
if (
array_key_exists($namespace, $project->getAttribute('services', []))
&& ! $project->getAttribute('services', [])[$namespace]
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles()))
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
@@ -435,7 +440,7 @@ Http::init()
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& ! $project->getAttribute('apis', [])['rest']
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles()))
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -474,110 +479,111 @@ Http::init()
Http::init()
->groups(['api'])
->inject('route')
->inject('request')
->inject('response')
->inject('project')
->inject('user')
->inject('timelimit')
->inject('devKey')
->inject('authorization')
->action(function (Route $route, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) {
$response->setUser($user);
$request->setUser($user);
$roles = $authorization->getRoles();
$shouldCheckAbuse = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'
&& ! $user->isKey($roles)
&& ! $user->isPrivileged($roles)
&& $devKey->isEmpty();
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
$closestLimit = null;
foreach ($abuseKeyLabel as $abuseKey) {
$isRateLimited = false;
try {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
foreach ($request->getParams() as $key => $value) {
if (! empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
}
if ($shouldCheckAbuse) {
$isRateLimited = $abuse->check();
}
} catch (\Throwable $th) {
\error_log((string) $th);
continue;
}
if ($isRateLimited) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
}
});
Http::init()
->groups(['api'])
->inject('route')
->inject('utopia')
->inject('request')
->inject('response')
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('auditContext')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
->inject('usage')
->inject('publisherForFunctions')
->inject('queueForFunctions')
->inject('queueForMails')
->inject('dbForProject')
->inject('timelimit')
->inject('resourceToken')
->inject('mode')
->inject('apiKey')
->inject('plan')
->inject('devKey')
->inject('telemetry')
->inject('platform')
->inject('authorization')
->inject('cacheControlForStorage')
->action(function (Route $route, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
$response->setUser($user);
$request->setUser($user);
$path = $route->getPath();
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
$path = $route->getMatchedPath();
$databaseType = match (true) {
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
default => '',
};
/*
* Abuse Check
*/
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$timeLimitArray = [];
$abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
$timeLimitArray[] = $timeLimit;
}
$closestLimit = null;
$roles = $authorization->getRoles();
$isPrivilegedUser = $user->isPrivileged($roles);
$isAppUser = $user->isApp($roles);
foreach ($timeLimitArray as $timeLimit) {
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
if (! empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
}
$enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
if (
$enabled // Abuse is enabled
&& ! $isAppUser // User is not API key
&& ! $isPrivilegedUser // User is not an admin
&& $devKey->isEmpty() // request doesn't not contain development key
&& $abuse->check() // Route is rate-limited
) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
}
/**
* TODO: (@loks0n)
* Avoid mutating the message across file boundaries - it's difficult to reason about at scale.
@@ -602,18 +608,30 @@ Http::init()
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTOR_TYPE_ADMIN : ACTOR_TYPE_USER);
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$auditContext->user = $userClone;
}
/* Auto-set projects */
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
$queueForMessaging->setProject($project);
$queueForFunctions->setProject($project);
$queueForBuilds->setProject($project);
$queueForMails->setProject($project);
/* Auto-set platforms */
$queueForFunctions->setPlatform($platform);
$queueForBuilds->setPlatform($platform);
$queueForMails->setPlatform($platform);
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
if ($useCache) {
$roles = $authorization->getRoles();
$isAppUser = $user->isKey($roles);
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($roles);
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
$key = $request->cacheIdentifier();
Span::add('storage.cache.key', $key);
@@ -625,7 +643,6 @@ Http::init()
$data = $cache->load($key, $timestamp);
if (! empty($data) && ! $cacheLog->isEmpty()) {
$cacheControl = \sprintf('private, max-age=%d', $timestamp);
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
$type = $parts[0];
@@ -634,7 +651,7 @@ Http::init()
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isPrivilegedUser = $user->isPrivileged($roles);
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -678,21 +695,6 @@ Http::init()
])));
}
}
if ($isImageTransformation) {
$cacheControl = $cacheControlForStorage(new StorageCacheControl(
source: CacheControl::SOURCE_CACHE,
user: $user,
maxAge: $timestamp,
project: $project,
bucket: $bucket,
file: $file,
resourceToken: $resourceToken,
fileSecurity: $fileSecurity,
cacheLog: $cacheLog,
route: $route,
));
}
}
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
@@ -705,7 +707,7 @@ Http::init()
}
$response
->addHeader('Cache-Control', $cacheControl)
->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($cacheLog->getAttribute('mimeType'));
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
@@ -747,11 +749,12 @@ Http::init()
*/
Http::shutdown()
->groups(['session'])
->inject('utopia')
->inject('request')
->inject('response')
->inject('project')
->inject('dbForProject')
->action(function (Request $request, Response $response, Document $project, Database $dbForProject) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, Database $dbForProject) {
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? 0;
if ($sessionLimit === 0) {
@@ -785,7 +788,7 @@ Http::shutdown()
Http::shutdown()
->groups(['api'])
->inject('route')
->inject('utopia')
->inject('request')
->inject('response')
->inject('project')
@@ -795,7 +798,11 @@ Http::shutdown()
->inject('publisherForAudits')
->inject('usage')
->inject('publisherForUsage')
->inject('publisherForFunctions')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
->inject('queueForMessaging')
->inject('queueForFunctions')
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
@@ -805,7 +812,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
->action(function (Route $route, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -834,15 +841,9 @@ Http::shutdown()
if (! empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
$publisherForFunctions->enqueue(FunctionMessage::fromEvent(
event: $queueForEvents->getEvent(),
params: $queueForEvents->getParams(),
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
userId: $queueForEvents->getUserId(),
payload: $queueForEvents->getPayload(),
platform: $queueForEvents->getPlatform(),
));
$queueForFunctions
->from($queueForEvents)
->trigger();
break;
}
}
@@ -861,6 +862,7 @@ Http::shutdown()
}
}
$route = $utopia->getRoute();
$requestParams = $route->getParamsValues();
/**
@@ -913,7 +915,7 @@ Http::shutdown()
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTOR_TYPE_ADMIN : ACTOR_TYPE_USER);
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$auditContext->user = $userClone;
} elseif ($auditContext->user === null || $auditContext->user->isEmpty()) {
@@ -928,7 +930,7 @@ Http::shutdown()
$user = new User([
'$id' => '',
'status' => true,
'type' => ACTOR_TYPE_GUEST,
'type' => ACTIVITY_TYPE_GUEST,
'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => 'Guest',
@@ -951,6 +953,22 @@ Http::shutdown()
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
}
if (! empty($queueForDeletes->getType())) {
$queueForDeletes->trigger();
}
if (! empty($queueForDatabase->getType())) {
$queueForDatabase->trigger();
}
if (! empty($queueForBuilds->getType())) {
$queueForBuilds->trigger();
}
if (! empty($queueForMessaging->getType())) {
$queueForMessaging->trigger();
}
// Cache label
$useCache = $route->getLabel('cache', false);
if ($useCache) {
+5 -4
View File
@@ -9,7 +9,6 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Http\Http;
use Utopia\Http\Route;
use Utopia\System\System;
Http::init()
@@ -33,13 +32,13 @@ Http::init()
Http::init()
->groups(['auth'])
->inject('route')
->inject('utopia')
->inject('request')
->inject('project')
->inject('geodb')
->inject('user')
->inject('authorization')
->action(function (Route $route, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) {
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) {
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
if (!empty($denylist && $project->getId() === 'console')) {
$countries = explode(',', $denylist);
@@ -50,8 +49,10 @@ Http::init()
}
}
$route = $utopia->match($request);
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
$isAppUser = $user->isKey($authorization->getRoles());
$isAppUser = $user->isApp($authorization->getRoles());
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;
+42 -34
View File
@@ -3,7 +3,7 @@
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/init/span.php';
$setRequestContext = require __DIR__ . '/init/resources/request.php';
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
@@ -26,7 +26,6 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\DI\Container;
use Utopia\Http\Adapter\Swoole\Server;
use Utopia\Http\Files;
use Utopia\Http\Http;
@@ -58,7 +57,7 @@ $container->set('pools', function ($register) {
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$swoole = new Server(
$swooleAdapter = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
settings: [
@@ -70,10 +69,10 @@ $swoole = new Server(
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
],
resources: $container,
container: $container,
);
$http = $swoole->getServer();
$http = $swooleAdapter->getServer();
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
@@ -191,11 +190,13 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
$container->set('bus', fn ($register) => $register->get('bus')->setResolver(fn (string $name) => $swoole->context()->get($name)), ['register']);
$container->set('bus', function ($register) use ($swooleAdapter) {
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
}, ['register']);
include __DIR__ . '/controllers/general.php';
function createDatabase(Container $resources, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
$max = 15;
$sleep = 2;
@@ -204,7 +205,7 @@ function createDatabase(Container $resources, string $resourceKey, string $dbNam
while (true) {
try {
$attempts++;
$resource = $resources->get($resourceKey);
$resource = $app->getResource($resourceKey);
/* @var $database Database */
$database = is_callable($resource) ? $resource() : $resource;
break; // exit loop on success
@@ -287,21 +288,23 @@ function createDatabase(Container $resources, string $resourceKey, string $dbNam
Span::current()?->finish();
}
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $container) {
/** @var \Utopia\Pools\Group $pools */
$pools = $container->get('pools');
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
$app = new Http($swooleAdapter, 'UTC');
go(function () use ($container, $pools) {
/** @var \Utopia\Pools\Group $pools */
$pools = $app->getResource('pools');
go(function () use ($app, $pools) {
/** @var array $collections */
$collections = Config::getParam('collections', []);
// create logs database first, `getLogsDB` is a callable.
createDatabase($container, 'getLogsDB', 'logs', $collections['logs'], $pools);
createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools);
// create appwrite database, `dbForPlatform` is a direct access call.
createDatabase($container, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $container) {
$authorization = $container->get('authorization');
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $app) {
$authorization = $app->getResource('authorization');
if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForPlatform);
@@ -413,7 +416,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
$documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
$cache = $container->get('cache');
$cache = $app->getResource('cache');
// All shared tables pools that need project metadata collections
$allSharedTables = \array_values(\array_unique(\array_filter([
@@ -499,7 +502,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
});
});
$swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoole, $setRequestContext) {
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
Span::init('http.request');
$request = new Request($utopiaRequest->getSwooleRequest());
@@ -519,18 +522,21 @@ $swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoo
return;
}
$app = new Http($swoole, 'UTC');
$app->context()->set('request', fn () => $request);
$app->context()->set('response', fn () => $response);
$app->context()->set('utopia', fn () => $app);
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('container', fn () => $requestContainer);
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
$setRequestContext($app->context());
$app = new Http($swooleAdapter, 'UTC');
$requestContainer->set('utopia', fn () => $app);
$registerRequestResources($requestContainer);
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
try {
$authorization = $app->context()->get('authorization');
$authorization = $app->getResource('authorization');
$request->setAuthorization($authorization);
$response->setAuthorization($authorization);
@@ -539,25 +545,25 @@ $swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoo
$app->run($request, $response);
$route = $app->match($request)?->route;
$route = $app->getRoute();
Span::add('http.path', $route?->getPath() ?? 'unknown');
} catch (\Throwable $th) {
Span::error($th);
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$logger = $app->context()->get("logger");
$logger = $app->getResource("logger");
if ($logger) {
try {
/** @var Utopia\Database\Document $user */
$user = $app->context()->get('user');
$user = $app->getResource('user');
} catch (\Throwable $_th) {
// All good, user is optional information for logger
}
$route = $app->match($request)?->route;
$route = $app->getRoute();
$log = $app->context()->get("log");
$log = $app->getResource("log");
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
@@ -636,16 +642,18 @@ $swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoo
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
$http->on(Constant::EVENT_TASK, function () use ($container) {
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
$lastSyncUpdate = null;
$app = new Http($swooleAdapter, 'UTC');
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $container->get('dbForPlatform');
$dbForPlatform = $app->getResource('dbForPlatform');
/** @var \Swoole\Table $riskyDomains */
$riskyDomains = $container->get('riskyDomains');
$riskyDomains = $app->getResource('riskyDomains');
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $container) {
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
try {
$time = DateTime::now();
$limit = 1000;
@@ -662,7 +670,7 @@ $http->on(Constant::EVENT_TASK, function () use ($container) {
}
$results = [];
try {
$authorization = $container->get('authorization');
$authorization = $app->getResource('authorization');
$results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) {
Console::error('rules ' . $th->getMessage());
@@ -712,4 +720,4 @@ $http->on(Constant::EVENT_TASK, function () use ($container) {
});
});
$swoole->start();
$swooleAdapter->start();
+17 -72
View File
@@ -1,11 +1,5 @@
<?php
use Appwrite\Platform\Modules\Advisor\Enums\InsightCTAMethod;
use Appwrite\Platform\Modules\Advisor\Enums\InsightCTAService;
use Appwrite\Platform\Modules\Advisor\Enums\InsightSeverity;
use Appwrite\Platform\Modules\Advisor\Enums\InsightStatus;
use Appwrite\Platform\Modules\Advisor\Enums\InsightType;
use Appwrite\Platform\Modules\Advisor\Enums\ReportType;
use Appwrite\Platform\Modules\Compute\Specification;
use Utopia\System\System;
@@ -50,15 +44,14 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4326;
const APP_VERSION_STABLE = '1.9.5';
const APP_CACHE_BUSTER = 4324;
const APP_VERSION_STABLE = '1.9.3';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
const APP_DATABASE_ATTRIBUTE_DATETIME = 'datetime';
const APP_DATABASE_ATTRIBUTE_URL = 'url';
const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange';
const APP_DATABASE_ATTRIBUTE_BIGINT_RANGE = 'bigintRange';
const APP_DATABASE_ATTRIBUTE_FLOAT_RANGE = 'floatRange';
const APP_DATABASE_ATTRIBUTE_POINT = 'point';
const APP_DATABASE_ATTRIBUTE_LINE = 'line';
@@ -159,14 +152,14 @@ const SESSION_PROVIDER_TOKEN = 'token';
const SESSION_PROVIDER_SERVER = 'server';
/**
* Actor that performed the request (user, admin, guest, or API key).
* Activity associated with user or the app.
*/
const ACTOR_TYPE_USER = 'user';
const ACTOR_TYPE_ADMIN = 'admin';
const ACTOR_TYPE_GUEST = 'guest';
const ACTOR_TYPE_KEY_PROJECT = 'keyProject';
const ACTOR_TYPE_KEY_ACCOUNT = 'keyAccount';
const ACTOR_TYPE_KEY_ORGANIZATION = 'keyOrganization';
const ACTIVITY_TYPE_USER = 'user';
const ACTIVITY_TYPE_ADMIN = 'admin';
const ACTIVITY_TYPE_GUEST = 'guest';
const ACTIVITY_TYPE_KEY_PROJECT = 'keyProject';
const ACTIVITY_TYPE_KEY_ACCOUNT = 'keyAccount';
const ACTIVITY_TYPE_KEY_ORGANIZATION = 'keyOrganization';
/**
* MFA
@@ -200,7 +193,7 @@ const BUILD_TYPE_RETRY = 'retry';
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
const DELETE_TYPE_COLLECTIONS = 'collections';
const DELETE_TYPE_TRANSACTIONS = 'transactions';
const DELETE_TYPE_TRANSACTION = 'transaction';
const DELETE_TYPE_EXPIRED_TRANSACTIONS = 'expired_transactions';
const DELETE_TYPE_PROJECTS = 'projects';
const DELETE_TYPE_SITES = 'sites';
@@ -228,7 +221,6 @@ const DELETE_TYPE_EXPIRED_TARGETS = 'invalid_targets';
const DELETE_TYPE_SESSION_TARGETS = 'session_targets';
const DELETE_TYPE_CSV_EXPORTS = 'csv_exports';
const DELETE_TYPE_MAINTENANCE = 'maintenance';
const DELETE_TYPE_REPORT = 'report';
// Rule statuses
const RULE_STATUS_CREATED = 'created'; // This is also the status when domain DNS verification fails.
@@ -262,6 +254,13 @@ const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length'];
const MESSAGE_TYPE_EMAIL = 'email';
const MESSAGE_TYPE_SMS = 'sms';
const MESSAGE_TYPE_PUSH = 'push';
// Notification types
const NOTIFICATION_TYPE_EMAIL = 'email';
const NOTIFICATION_TYPE_SMS = 'sms';
const NOTIFICATION_TYPE_PUSH = 'push';
const NOTIFICATION_TYPE_CONSOLE = 'console';
const NOTIFICATION_TYPE_WEBHOOK = 'webhook';
const RESOURCE_TYPE_ALERTS = 'alerts';
// API key types
const API_KEY_STANDARD = 'standard';
const API_KEY_EPHEMERAL = 'ephemeral';
@@ -394,7 +393,6 @@ const METRIC_NETWORK_OUTBOUND = 'network.outbound';
const METRIC_MAU = 'users.mau';
const METRIC_DAU = 'users.dau';
const METRIC_WAU = 'users.wau';
const METRIC_USERS_PRESENCE = 'users.presence';
const METRIC_WEBHOOKS = 'webhooks';
const METRIC_PLATFORMS = 'platforms';
const METRIC_PROVIDERS = 'providers';
@@ -432,55 +430,6 @@ const RESOURCE_TYPE_MESSAGES = 'messages';
const RESOURCE_TYPE_EXECUTIONS = 'executions';
const RESOURCE_TYPE_VCS = 'vcs';
const RESOURCE_TYPE_EMBEDDINGS_TEXT = 'embeddingsText';
const RESOURCE_TYPE_INSIGHTS = 'insights';
const RESOURCE_TYPE_REPORTS = 'reports';
// Insight types — engine-specific so the CTA action can reference the right public API.
const ADVISOR_INSIGHT_TYPES = [
InsightType::DATABASE_INDEX->value, // legacy databases.createIndex
InsightType::TABLES_DB_INDEX->value, // tablesDB.createIndex
InsightType::DOCUMENTS_DB_INDEX->value, // documentsDB.createIndex
InsightType::VECTORS_DB_INDEX->value, // vectorsDB.createIndex
InsightType::DATABASE_PERFORMANCE->value,
InsightType::SITE_PERFORMANCE->value,
InsightType::SITE_ACCESSIBILITY->value,
InsightType::SITE_SEO->value,
InsightType::FUNCTION_PERFORMANCE->value,
];
// Public API services (SDK namespaces) that an insight CTA's `service` can reference.
// Analyzers must pick the one matching the engine the resource lives in.
const ADVISOR_CTA_SERVICES = [
InsightCTAService::DATABASES->value, // legacy
InsightCTAService::TABLES_DB->value,
InsightCTAService::DOCUMENTS_DB->value,
InsightCTAService::VECTORS_DB->value,
];
// Public API method names that an insight CTA's `method` can reference for index suggestions.
const ADVISOR_CTA_METHODS = [
InsightCTAMethod::CREATE_INDEX->value,
];
// Insight severities
const ADVISOR_SEVERITIES = [
InsightSeverity::INFO->value,
InsightSeverity::WARNING->value,
InsightSeverity::CRITICAL->value,
];
// Insight statuses
const ADVISOR_STATUSES = [
InsightStatus::ACTIVE->value,
InsightStatus::DISMISSED->value,
];
// Report types
const ADVISOR_REPORT_TYPES = [
ReportType::LIGHTHOUSE->value,
ReportType::AUDIT->value,
ReportType::DATABASE_ANALYZER->value,
];
// Resource types for Tokens
const TOKENS_RESOURCE_TYPE_FILES = 'files';
@@ -515,7 +464,3 @@ const CSV_ALLOWED_DATABASE_TYPES = [
DATABASE_TYPE_TABLESDB,
DATABASE_TYPE_VECTORSDB
];
const VCS_DEPLOYMENT_SKIP_PATTERNS = [
'[skip ci]',
];
-14
View File
@@ -475,17 +475,3 @@ Database::addFilter(
]));
}
);
Database::addFilter(
'subQueryReportInsights',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database->find('insights', [
Query::equal('projectInternalId', [$document->getAttribute('projectInternalId')]),
Query::equal('reportInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
-7
View File
@@ -36,13 +36,6 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) {
return new Range($min, $max, Range::TYPE_INTEGER);
}, Database::VAR_INTEGER);
// BigInt uses a dedicated bigintRange format name to avoid clobbering `intRange`.
Structure::addFormat(APP_DATABASE_ATTRIBUTE_BIGINT_RANGE, function ($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
return new Range($min, $max, Range::TYPE_INTEGER);
}, Database::VAR_BIGINT);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
+4 -24
View File
@@ -2,6 +2,7 @@
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Account;
use Appwrite\Utopia\Response\Model\Alert;
use Appwrite\Utopia\Response\Model\AlgoArgon2;
use Appwrite\Utopia\Response\Model\AlgoBcrypt;
use Appwrite\Utopia\Response\Model\AlgoMd5;
@@ -11,7 +12,6 @@ use Appwrite\Utopia\Response\Model\AlgoScryptModified;
use Appwrite\Utopia\Response\Model\AlgoSha;
use Appwrite\Utopia\Response\Model\Any;
use Appwrite\Utopia\Response\Model\Attribute;
use Appwrite\Utopia\Response\Model\AttributeBigInt;
use Appwrite\Utopia\Response\Model\AttributeBoolean;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\AttributeEmail;
@@ -38,7 +38,6 @@ use Appwrite\Utopia\Response\Model\Branch;
use Appwrite\Utopia\Response\Model\Bucket;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\Column;
use Appwrite\Utopia\Response\Model\ColumnBigInt;
use Appwrite\Utopia\Response\Model\ColumnBoolean;
use Appwrite\Utopia\Response\Model\ColumnDatetime;
use Appwrite\Utopia\Response\Model\ColumnEmail;
@@ -92,8 +91,6 @@ use Appwrite\Utopia\Response\Model\HealthTime;
use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\Identity;
use Appwrite\Utopia\Response\Model\Index;
use Appwrite\Utopia\Response\Model\Insight;
use Appwrite\Utopia\Response\Model\InsightCTA;
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\JWT;
use Appwrite\Utopia\Response\Model\Key;
@@ -174,18 +171,13 @@ use Appwrite\Utopia\Response\Model\PolicySessionInvalidation;
use Appwrite\Utopia\Response\Model\PolicySessionLimit;
use Appwrite\Utopia\Response\Model\PolicyUserLimit;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Presence;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\ProjectAuthMethod;
use Appwrite\Utopia\Response\Model\ProjectProtocol;
use Appwrite\Utopia\Response\Model\ProjectService;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
use Appwrite\Utopia\Response\Model\Report;
use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
@@ -215,7 +207,6 @@ use Appwrite\Utopia\Response\Model\UsageDocumentsDB;
use Appwrite\Utopia\Response\Model\UsageDocumentsDBs;
use Appwrite\Utopia\Response\Model\UsageFunction;
use Appwrite\Utopia\Response\Model\UsageFunctions;
use Appwrite\Utopia\Response\Model\UsagePresence;
use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageSite;
use Appwrite\Utopia\Response\Model\UsageSites;
@@ -239,7 +230,6 @@ Response::setModel(new ErrorDev());
// Lists
Response::setModel(new BaseList('Rows List', Response::MODEL_ROW_LIST, 'rows', Response::MODEL_ROW));
Response::setModel(new BaseList('Documents List', Response::MODEL_DOCUMENT_LIST, 'documents', Response::MODEL_DOCUMENT));
Response::setModel(new BaseList('Presences List', Response::MODEL_PRESENCE_LIST, 'presences', Response::MODEL_PRESENCE));
Response::setModel(new BaseList('Tables List', Response::MODEL_TABLE_LIST, 'tables', Response::MODEL_TABLE));
Response::setModel(new BaseList('Collections List', Response::MODEL_COLLECTION_LIST, 'collections', Response::MODEL_COLLECTION));
Response::setModel(new BaseList('Databases List', Response::MODEL_DATABASE_LIST, 'databases', Response::MODEL_DATABASE));
@@ -248,6 +238,7 @@ Response::setModel(new BaseList('Column Indexes List', Response::MODEL_COLUMN_IN
Response::setModel(new BaseList('Users List', Response::MODEL_USER_LIST, 'users', Response::MODEL_USER));
Response::setModel(new BaseList('Sessions List', Response::MODEL_SESSION_LIST, 'sessions', Response::MODEL_SESSION));
Response::setModel(new BaseList('Identities List', Response::MODEL_IDENTITY_LIST, 'identities', Response::MODEL_IDENTITY));
Response::setModel(new BaseList('Alerts List', Response::MODEL_ALERT_LIST, 'alerts', Response::MODEL_ALERT));
Response::setModel(new BaseList('Logs List', Response::MODEL_LOG_LIST, 'logs', Response::MODEL_LOG));
Response::setModel(new BaseList('Files List', Response::MODEL_FILE_LIST, 'files', Response::MODEL_FILE));
Response::setModel(new BaseList('Buckets List', Response::MODEL_BUCKET_LIST, 'buckets', Response::MODEL_BUCKET));
@@ -266,7 +257,7 @@ Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIS
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT));
Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION));
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, true));
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false));
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, true));
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
@@ -297,8 +288,6 @@ Response::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICA
Response::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
Response::setModel(new BaseList('VectorsDB Collections List', Response::MODEL_VECTORSDB_COLLECTION_LIST, 'collections', Response::MODEL_VECTORSDB_COLLECTION));
Response::setModel(new BaseList('Embedding list', Response::MODEL_EMBEDDING_LIST, 'embeddings', Response::MODEL_EMBEDDING));
Response::setModel(new BaseList('Insights List', Response::MODEL_INSIGHT_LIST, 'insights', Response::MODEL_INSIGHT));
Response::setModel(new BaseList('Reports List', Response::MODEL_REPORT_LIST, 'reports', Response::MODEL_REPORT));
// Entities
Response::setModel(new Database());
@@ -310,7 +299,6 @@ Response::setModel(new Attribute());
Response::setModel(new AttributeList());
Response::setModel(new AttributeString());
Response::setModel(new AttributeInteger());
Response::setModel(new AttributeBigInt());
Response::setModel(new AttributeFloat());
Response::setModel(new AttributeBoolean());
Response::setModel(new AttributeEmail());
@@ -344,7 +332,6 @@ Response::setModel(new Column());
Response::setModel(new ColumnList());
Response::setModel(new ColumnString());
Response::setModel(new ColumnInteger());
Response::setModel(new ColumnBigInt());
Response::setModel(new ColumnFloat());
Response::setModel(new ColumnBoolean());
Response::setModel(new ColumnEmail());
@@ -364,7 +351,6 @@ Response::setModel(new Index());
Response::setModel(new ColumnIndex());
Response::setModel(new Row());
Response::setModel(new ModelDocument());
Response::setModel(new Presence());
Response::setModel(new Log());
Response::setModel(new User());
Response::setModel(new AlgoMd5());
@@ -378,6 +364,7 @@ Response::setModel(new Account());
Response::setModel(new Preferences());
Response::setModel(new Session());
Response::setModel(new Identity());
Response::setModel(new Alert());
Response::setModel(new Token());
Response::setModel(new JWT());
Response::setModel(new Locale());
@@ -409,9 +396,6 @@ Response::setModel(new FrameworkAdapter());
Response::setModel(new Deployment());
Response::setModel(new Execution());
Response::setModel(new Project());
Response::setModel(new ProjectAuthMethod());
Response::setModel(new ProjectService());
Response::setModel(new ProjectProtocol());
Response::setModel(new Webhook());
Response::setModel(new Key());
Response::setModel(new EphemeralKey());
@@ -493,7 +477,6 @@ Response::setModel(new UsageDatabase());
Response::setModel(new UsageTable());
Response::setModel(new UsageCollection());
Response::setModel(new UsageUsers());
Response::setModel(new UsagePresence());
Response::setModel(new UsageStorage());
Response::setModel(new UsageBuckets());
Response::setModel(new UsageFunctions());
@@ -525,9 +508,6 @@ Response::setModel(new Target());
Response::setModel(new Migration());
Response::setModel(new MigrationReport());
Response::setModel(new MigrationFirebaseProject());
Response::setModel(new Insight());
Response::setModel(new InsightCTA());
Response::setModel(new Report());
// Tests (keep last)
Response::setModel(new Mock());
+1 -15
View File
@@ -240,12 +240,6 @@ $register->set('pools', function () {
'multiple' => true,
'schemes' => ['redis'],
],
'lock' => [
'type' => 'lock',
'dsns' => $fallbackForRedis,
'multiple' => false,
'schemes' => ['redis'],
],
];
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
@@ -338,13 +332,7 @@ $register->set('pools', function () {
$poolAdapter = System::getEnv('_APP_POOL_ADAPTER', default: 'stack') === 'swoole' ? new SwoolePool() : new StackPool();
// PubSub workers hold one long-lived subscribed connection and also need
// spare capacity for publishes from the same process.
$connectionPoolSize = $type === 'pubsub'
? max(2, $poolSize)
: $poolSize;
$pool = new Pool($poolAdapter, $name, $connectionPoolSize, function () use ($type, $resource, $dsn) {
$pool = new Pool($poolAdapter, $name, $poolSize, function () use ($type, $resource, $dsn) {
// Get Adapter
switch ($type) {
case 'database':
@@ -381,8 +369,6 @@ $register->set('pools', function () {
}
return $adapter;
case 'lock':
return $resource();
default:
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation.");
}
+61 -109
View File
@@ -2,19 +2,12 @@
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Audit as AuditPublisher;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot as ScreenshotPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Platform\Modules\Storage\Config\StorageCacheControl;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
@@ -29,7 +22,6 @@ use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Lock\Distributed;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Publisher;
@@ -53,93 +45,6 @@ global $register;
global $container;
$container = new Container();
$container->set('console', fn () => new Document(Config::getParam('console')), []);
$container->set('executor', fn () => new Executor(), []);
$container->set('telemetry', fn () => new NoTelemetry(), []);
$container->set('publisher', fn (Group $pools) => new BrokerPool(publisher: $pools->get('publisher')), ['pools']);
$container->set('publisherDatabases', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherFunctions', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherMigrations', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherMails', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherDeletes', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherMessaging', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherWebhooks', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherForAudits', fn (Publisher $publisher) => new AuditPublisher(
$publisher,
new Queue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForScreenshots', fn (Publisher $publisher) => new ScreenshotPublisher(
$publisher,
new Queue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForExecutions', fn (Publisher $publisher) => new ExecutionPublisher(
$publisher,
new Queue(System::getEnv('_APP_EXECUTIONS_QUEUE_NAME', Event::EXECUTIONS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$container->set('publisherForMigrations', fn (Publisher $publisher) => new MigrationPublisher(
$publisher,
new Queue(System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForBuilds', fn (Publisher $publisher) => new BuildPublisher(
$publisher,
new Queue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForDatabase', fn (Publisher $publisherDatabases) => new DatabasePublisher(
$publisherDatabases,
new Queue(System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME))
), ['publisherDatabases']);
$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
$publisher,
new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMails', fn (Publisher $publisher) => new MailPublisher(
$publisher,
new Queue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMessaging', fn (Publisher $publisher) => new MessagingPublisher(
$publisher,
new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
), ['publisher']);
$container->set('logger', function ($register) {
return $register->get('logger');
}, ['register']);
@@ -154,6 +59,59 @@ $container->set('localeCodes', function () {
return array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', []));
});
// Queues - shared infrastructure (stateless pool wrappers)
$container->set('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
$container->set('publisherDatabases', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherFunctions', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMigrations', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMails', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherDeletes', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMessaging', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherWebhooks', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherForAudits', fn (Publisher $publisher) => new AuditPublisher(
$publisher,
new Queue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForScreenshots', fn (Publisher $publisher) => new ScreenshotPublisher(
$publisher,
new Queue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForExecutions', fn (Publisher $publisher) => new ExecutionPublisher(
$publisher,
new Queue(System::getEnv('_APP_EXECUTIONS_QUEUE_NAME', Event::EXECUTIONS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMigrations', fn (Publisher $publisher) => new MigrationPublisher(
$publisher,
new Queue(System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
), ['publisher']);
/**
* Platform configuration
@@ -162,6 +120,10 @@ $container->set('platform', function () {
return Config::getParam('platform', []);
}, []);
$container->set('console', function () {
return new Document(Config::getParam('console'));
}, []);
$container->set('authorization', function () {
return new Authorization();
}, []);
@@ -220,6 +182,8 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
};
}, ['pools', 'cache', 'authorization']);
$container->set('telemetry', fn () => new NoTelemetry());
$container->set('cache', function (Group $pools, Telemetry $telemetry) {
$list = Config::getParam('pools-cache', []);
$adapters = [];
@@ -234,10 +198,6 @@ $container->set('cache', function (Group $pools, Telemetry $telemetry) {
return $cache;
}, ['pools', 'telemetry']);
$container->set('cacheControlForStorage', fn () => function (StorageCacheControl $config): string {
return \sprintf('private, max-age=%d', $config->maxAge);
});
$container->set('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
@@ -253,16 +213,6 @@ $container->set('redis', function () {
return $redis;
});
$container->set('locks', function (Group $pools) {
return function (string $key, int $ttl, callable $callback, float $timeout = 0.0) use ($pools): mixed {
return $pools->get('lock')->use(function (\Redis $redis) use ($key, $ttl, $callback, $timeout) {
$lock = new Distributed($redis, $key, ttl: $ttl);
return $lock->withLock($callback, timeout: $timeout);
});
};
}, ['pools']);
$container->set('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
@@ -420,3 +370,5 @@ $container->set(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
$container->set('executor', fn () => new Executor());
+139 -93
View File
@@ -4,10 +4,15 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Build;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Notification;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
@@ -47,7 +52,6 @@ use Utopia\Locale\Locale;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Storage\Device;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
@@ -59,18 +63,26 @@ use Utopia\Validator\WhiteList;
* These resources depend (directly or transitively) on request/response
* and must be fresh for each HTTP request.
*/
return function (Container $context): void {
$context->set('utopia:graphql', fn ($utopia) => $utopia, ['utopia']);
return function (Container $container): void {
$container->set('utopia:graphql', function ($utopia) {
return $utopia;
}, ['utopia']);
$context->set('log', fn () => new Log(), []);
$container->set('log', fn () => new Log(), []);
$context->set('logger', fn ($register) => $register->get('logger'), ['register']);
$container->set('logger', function ($register) {
return $register->get('logger');
}, ['register']);
$context->set('authorization', fn () => new Authorization(), []);
$container->set('authorization', function () {
return new Authorization();
}, []);
$context->set('store', fn (): Store => new Store(), []);
$container->set('store', function (): Store {
return new Store();
}, []);
$context->set('proofForPassword', function (): Password {
$container->set('proofForPassword', function (): Password {
$hash = new Argon2();
$hash
->setMemoryCost(7168)
@@ -84,21 +96,21 @@ return function (Container $context): void {
return $password;
});
$context->set('proofForToken', function (): Token {
$container->set('proofForToken', function (): Token {
$token = new Token();
$token->setHash(new Sha());
return $token;
});
$context->set('proofForCode', function (): Code {
$container->set('proofForCode', function (): Code {
$code = new Code();
$code->setHash(new Sha());
return $code;
});
$context->set('locale', function () {
$container->set('locale', function () {
$locale = new Locale(System::getEnv('_APP_LOCALE', 'en'));
$locale->setFallback(System::getEnv('_APP_LOCALE', 'en'));
@@ -106,17 +118,44 @@ return function (Container $context): void {
});
// Per-request queue resources (stateful, accumulate event data during request)
$context->set('queueForEvents', fn (Publisher $publisher) => new Event($publisher), ['publisher']);
$context->set('queueForWebhooks', fn (Publisher $publisher) => new Webhook($publisher), ['publisher']);
$context->set('queueForRealtime', fn () => new Realtime(), []);
$context->set('usage', fn () => new UsageContext(), []);
$context->set('auditContext', fn () => new AuditContext(), []);
$context->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$context->set('eventProcessor', fn () => new EventProcessor(), []);
$context->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$container->set('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
$container->set('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
$container->set('queueForNotifications', function (Publisher $publisher) {
return new Notification($publisher);
}, ['publisher']);
$container->set('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
$container->set('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
$container->set('usage', function () {
return new UsageContext();
}, []);
$container->set('auditContext', fn () => new AuditContext(), []);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('eventProcessor', function () {
return new EventProcessor();
}, []);
$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);
@@ -134,7 +173,7 @@ return function (Container $context): void {
return $database;
}, ['pools', 'cache', 'authorization']);
$context->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$adapters = [];
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$adapters) {
@@ -191,7 +230,7 @@ return function (Container $context): void {
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$context->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$adapter) {
@@ -223,7 +262,7 @@ return function (Container $context): void {
/**
* List of allowed request hostnames for the request.
*/
$context->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$container->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$allowed = [...($platform['hostnames'] ?? [])];
/* Add platform configured hostnames */
@@ -267,7 +306,7 @@ return function (Container $context): void {
/**
* List of allowed request schemes for the request.
*/
$context->set('allowedSchemes', function (array $platform, Document $project) {
$container->set('allowedSchemes', function (array $platform, Document $project) {
$allowed = [...($platform['schemas'] ?? [])];
if (! $project->isEmpty() && $project->getId() !== 'console') {
@@ -287,7 +326,7 @@ return function (Container $context): void {
/**
* Whether the request origin is verified against the request hostname.
*/
$context->set('domainVerification', function (Request $request) {
$container->set('domainVerification', function (Request $request) {
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string) $origin);
@@ -299,7 +338,7 @@ return function (Container $context): void {
/**
* Cookie domain for the current request.
*/
$context->set('cookieDomain', function (Request $request, Document $project) {
$container->set('cookieDomain', function (Request $request, Document $project) {
$localHosts = ['localhost', 'localhost:' . $request->getPort()];
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
@@ -333,7 +372,7 @@ return function (Container $context): void {
/**
* Rule associated with a request origin.
*/
$context->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$container->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
@@ -383,7 +422,7 @@ return function (Container $context): void {
/**
* CORS service
*/
$context->set('cors', function (array $allowedHostnames) {
$container->set('cors', function (array $allowedHostnames) {
$corsConfig = Config::getParam('cors');
return new Cors(
@@ -395,23 +434,23 @@ return function (Container $context): void {
);
}, ['allowedHostnames']);
$context->set(
'originValidator',
fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
? new Origin($allowedHostnames, $allowedSchemes)
: new URL(),
['devKey', 'allowedHostnames', 'allowedSchemes']
);
$container->set('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (! $devKey->isEmpty()) {
return new URL();
}
$context->set(
'redirectValidator',
fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
? new Redirect($allowedHostnames, $allowedSchemes)
: new URL(),
['devKey', 'allowedHostnames', 'allowedSchemes']
);
return new Origin($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
$context->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
$container->set('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (! $devKey->isEmpty()) {
return new URL();
}
return new Redirect($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
$container->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
/**
* Handles user authentication and session validation.
*
@@ -582,7 +621,7 @@ return function (Container $context): void {
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
$context->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
$container->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
@@ -596,7 +635,7 @@ return function (Container $context): void {
// These endpoints moved from /v1/projects/:projectId/<resource> to /v1/<resource>
// When accessed via the old alias path, extract projectId from the URI
$deprecatedProjectPathPrefix = '/v1/projects/';
$route = $utopia->match($request)?->route;
$route = $utopia->match($request);
if (!empty($route)) {
$isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) &&
!\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix);
@@ -615,7 +654,7 @@ return function (Container $context): void {
return $project;
}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']);
$context->set('session', function (User $user, Store $store, Token $proofForToken) {
$container->set('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) {
return;
}
@@ -636,7 +675,7 @@ return function (Container $context): void {
return;
}, ['user', 'store', 'proofForToken']);
$context->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
$container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -692,7 +731,7 @@ return function (Container $context): void {
* Accounts can be created in many ways beyond `createAccount`
* (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here.
*/
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
// Only trigger events for user creation with the database listener.
if ($document->getCollection() !== 'users') {
return;
@@ -704,15 +743,9 @@ return function (Container $context): void {
->setPayload($response->output($document, Response::MODEL_USER));
// Trigger functions, webhooks, and realtime events
$publisherForFunctions->enqueue(FunctionMessage::fromEvent(
event: $queueForEvents->getEvent(),
params: $queueForEvents->getParams(),
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
userId: $queueForEvents->getUserId(),
payload: $queueForEvents->getPayload(),
platform: $queueForEvents->getPlatform(),
));
$queueForFunctions
->from($queueForEvents)
->trigger();
/** Trigger webhooks events only if a project has them enabled */
if (! empty($project->getAttribute('webhooks'))) {
@@ -892,6 +925,7 @@ return function (Container $context): void {
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($publisher);
$queueForFunctions = new Func($publisherFunctions);
$queueForWebhooks = new Webhook($publisherWebhooks);
$queueForRealtime = new Realtime();
@@ -906,7 +940,7 @@ return function (Container $context): void {
$document,
$response,
$queueForEventsClone->from($queueForEvents),
$publisherForFunctions,
$queueForFunctions->from($queueForEvents),
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
))
@@ -915,9 +949,9 @@ return function (Container $context): void {
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database));
return $database;
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'publisherForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
$context->set('schema', function ($utopia, $dbForProject, $authorization) {
$container->set('schema', function ($utopia, $dbForProject, $authorization) {
$complexity = function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []);
@@ -1004,9 +1038,13 @@ return function (Container $context): void {
);
}, ['utopia', 'dbForProject', 'authorization']);
$context->set('audit', fn ($dbForProject) => new Audit(new AdapterDatabase($dbForProject)), ['dbForProject']);
$container->set('audit', function ($dbForProject) {
$adapter = new AdapterDatabase($dbForProject);
$context->set('mode', function ($request, Document $project) {
return new Audit($adapter);
}, ['dbForProject']);
$container->set('mode', function ($request, Document $project) {
/** @var Appwrite\Utopia\Request $request */
/**
@@ -1024,7 +1062,7 @@ return function (Container $context): void {
return $mode;
}, ['request', 'project']);
$context->set('requestTimestamp', function ($request) {
$container->set('requestTimestamp', function ($request) {
// TODO: Move this to the Request class itself
$timestampHeader = $request->getHeader('x-appwrite-timestamp');
$requestTimestamp = null;
@@ -1039,7 +1077,7 @@ return function (Container $context): void {
return $requestTimestamp;
}, ['request']);
$context->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$container->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
// Check if given key match project's development keys
@@ -1088,12 +1126,12 @@ return function (Container $context): void {
return $key;
}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']);
$context->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
$container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
$teamInternalId = '';
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
} else {
$route = $utopia->match($request)?->route;
$route = $utopia->match($request);
$path = ! empty($route) ? $route->getPath() : $request->getURI();
$orgHeader = $request->getHeader('x-appwrite-organization', '');
if (str_starts_with($path, '/v1/projects/:projectId')) {
@@ -1131,7 +1169,7 @@ return function (Container $context): void {
return $team;
}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']);
$context->set('previewHostname', function (Request $request, ?Key $apiKey) {
$container->set('previewHostname', function (Request $request, ?Key $apiKey) {
$allowed = false;
if (Http::isDevelopment()) {
@@ -1150,7 +1188,7 @@ return function (Container $context): void {
return '';
}, ['request', 'apiKey']);
$context->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$container->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$key = $request->getHeader('x-appwrite-key');
if (empty($key)) {
@@ -1184,7 +1222,7 @@ return function (Container $context): void {
return $key;
}, ['request', 'project', 'team', 'user']);
$context->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$container->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$tokenJWT = $request->getParam('token');
if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication
@@ -1251,10 +1289,10 @@ return function (Container $context): void {
return new Document([]);
}, ['project', 'dbForProject', 'request', 'authorization']);
$context->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
$container->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
$databaseDSN = $database->getAttribute('database') ?: $project->getAttribute('database', '');
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
try {
@@ -1413,27 +1451,35 @@ return function (Container $context): void {
}, ['pools', 'cache', 'project', 'request', 'usage', 'authorization']);
$context->set(
'transactionState',
fn (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) => new TransactionState($dbForProject, $authorization, $getDatabasesDB),
['dbForProject', 'authorization', 'getDatabasesDB']
);
$container->set('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) {
return new TransactionState($dbForProject, $authorization, $getDatabasesDB);
}, ['dbForProject', 'authorization', 'getDatabasesDB']);
$context->set(
'executionsRetentionCount',
fn (Document $project, array $plan) => ($project->getId() === 'console' || empty($plan))
? 0
: (int) ($plan['executionsRetentionCount'] ?? 100),
['project', 'plan']
);
$container->set('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
$context->set('deviceForFiles', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForSites', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForMigrations', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForFunctions', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForBuilds', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())), ['project', 'telemetry']);
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
$context->set('embeddingAgent', function ($register) {
$container->set('deviceForFiles', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForSites', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForMigrations', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFunctions', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForBuilds', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('embeddingAgent', function ($register) {
$adapter = new Ollama();
$adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed'));
$adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000'));
+1 -20
View File
@@ -3,30 +3,11 @@
use Utopia\Span\Exporter;
use Utopia\Span\Span;
use Utopia\Span\Storage;
use Utopia\System\System;
Span::setStorage(new Storage\Coroutine());
// Resolve trace filters once at boot to avoid repeated env lookups per span.
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$traceEnabled = $traceProjectId !== '' || $traceFunctionId !== '';
Span::addExporter(new Exporter\Pretty(), function (Span $span) use ($traceEnabled, $traceProjectId, $traceFunctionId): bool {
Span::addExporter(new Exporter\Pretty(), function (Span $span): bool {
if (\str_starts_with($span->getAction(), 'listener.')) {
return $span->getError() !== null;
}
// Selective tracing: when _APP_TRACE_PROJECT_ID / _APP_TRACE_FUNCTION_ID are set,
// only export spans tagged with matching project.id / function.id.
if ($traceEnabled) {
if ($traceProjectId !== '' && $span->get('project.id') !== $traceProjectId) {
return false;
}
if ($traceFunctionId !== '' && $span->get('function.id') !== $traceFunctionId) {
return false;
}
}
return true;
});
+35
View File
@@ -1,6 +1,13 @@
<?php
use Appwrite\Event\Build;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Notification;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
@@ -324,6 +331,30 @@ return function (Container $container): void {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
}, []);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
$container->set('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
$container->set('queueForNotifications', function (Publisher $publisher) {
return new Notification($publisher);
}, ['publisher']);
$container->set('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
@@ -332,6 +363,10 @@ return function (Container $container): void {
return new Webhook($publisher);
}, ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
+336 -320
View File
@@ -1,20 +1,10 @@
<?php
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime as QueueRealtime;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Network\Validator\Origin;
use Appwrite\Presences\State as PresenceState;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use Appwrite\Realtime\Message\Dispatcher as MessageDispatcher;
use Appwrite\Realtime\Message\Handlers\Authentication as AuthenticationHandler;
use Appwrite\Realtime\Message\Handlers\Ping as PingHandler;
use Appwrite\Realtime\Message\Handlers\Presence as PresenceHandler;
use Appwrite\Realtime\Message\Handlers\Subscribe as SubscribeHandler;
use Appwrite\Realtime\Message\Handlers\Unsubscribe as UnsubscribeHandler;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
@@ -26,6 +16,9 @@ use Swoole\Table;
use Swoole\Timer;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@@ -35,9 +28,7 @@ use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Exception\Timeout as TimeoutException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
@@ -46,8 +37,6 @@ use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Queue;
use Utopia\Registry\Registry;
use Utopia\Span\Span;
use Utopia\System\System;
@@ -78,38 +67,6 @@ set_exception_handler(function (\Throwable $e) {
));
});
global $container;
if (!$container->has('pools')) {
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
}
if (!$container->has('publisherForUsage')) {
$container->set('publisherForUsage', function (Group $pools): UsagePublisher {
$statsUsageConnection = System::getEnv('_APP_CONNECTIONS_QUEUE_STATS_USAGE', '');
$publisherPoolName = 'publisher';
if (!empty($statsUsageConnection)) {
try {
$pools->get('publisher_' . $statsUsageConnection);
$publisherPoolName = 'publisher_' . $statsUsageConnection;
} catch (Throwable) {
// Fallback to default publisher pool when custom one is unavailable.
}
}
return new UsagePublisher(
new BrokerPool(publisher: $pools->get($publisherPoolName)),
new Queue(System::getEnv(
'_APP_STATS_USAGE_QUEUE_NAME',
QueueEvent::STATS_USAGE_QUEUE_NAME
))
);
}, ['pools']);
}
// Allows overriding
if (!function_exists('getConsoleDB')) {
function getConsoleDB(): Database
@@ -277,7 +234,6 @@ if (!function_exists('getRealtime')) {
}
}
if (!function_exists('getTelemetry')) {
function getTelemetry(int $workerId): Utopia\Telemetry\Adapter
{
@@ -291,58 +247,18 @@ if (!function_exists('getTelemetry')) {
}
}
if (!function_exists('getQueueForEvents')) {
function getQueueForEvents(): QueueEvent
{
$ctx = Coroutine::getContext();
if (!isset($ctx['queueForEvents'])) {
global $register;
/** @var Group $pools */
$pools = $register->get('pools');
$ctx['queueForEvents'] = new QueueEvent(new BrokerPool(
publisher: $pools->get('publisher')
));
}
return $ctx['queueForEvents'];
}
}
if (!function_exists('getQueueForRealtime')) {
function getQueueForRealtime(): QueueRealtime
{
$ctx = Coroutine::getContext();
if (!isset($ctx['queueForRealtime'])) {
$ctx['queueForRealtime'] = new QueueRealtime();
}
return $ctx['queueForRealtime'];
}
}
if (!function_exists('triggerStats')) {
function triggerStats(array $event, string $projectId): void
{
}
}
if (!function_exists('checkForProjectUsage')) {
function checkForProjectUsage(Document $project): void
{
}
}
global $container;
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
$realtime = getRealtime();
$presenceState = new PresenceState();
$messageDispatcher = (new MessageDispatcher())
->addHandler(new PingHandler())
->addHandler(new AuthenticationHandler())
->addHandler(new SubscribeHandler())
->addHandler(new UnsubscribeHandler())
->addHandler(new PresenceHandler());
/**
* Table for statistics across all workers.
@@ -376,16 +292,7 @@ if (!function_exists('logError')) {
$logger = $register->get('realtimeLogger');
// Match HTTP semantics (app/controllers/general.php): AppwriteException uses its
// configured publish flag; everything else publishes only for code 0 or >= 500.
// Without this, expected client errors (e.g. Utopia DB Authorization) hit Sentry.
if ($error instanceof AppwriteException) {
$publish = $error->isPublishable();
} else {
$publish = $error->getCode() === 0 || $error->getCode() >= 500;
}
if ($logger && $publish) {
if ($logger && !$error instanceof Exception) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
@@ -705,16 +612,6 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
}
}
// Strip deleted presences from in-memory connection state so onClose doesn't
// re-fire delete events for rows already removed via HTTP DELETE.
$deletedPresenceId = Realtime::extractDeletedPresenceId($event);
if ($deletedPresenceId !== null) {
$realtime->removePresenceFromConnections(
(string) ($event['project'] ?? ''),
$deletedPresenceId,
);
}
$receivers = $realtime->getSubscribers($event);
if (System::getEnv('_APP_ENV', 'production') === 'development' && !empty($receivers)) {
@@ -831,8 +728,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$success = false;
Span::init('realtime.open');
Span::add('realtime.connection.id', $connection);
Span::add('realtime.inbound_bytes', $rawSize);
Span::add('realtime.connectionId', $connection);
Span::add('realtime.inboundBytes', $rawSize);
if (!empty($request->getOrigin())) {
Span::add('realtime.origin', $request->getOrigin());
}
@@ -858,7 +755,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true;
if (
!$websocketEnabled
&& !($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles()))
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -1001,16 +898,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$success = true;
} catch (Throwable $th) {
Span::error($th);
// Convert known Utopia DB exceptions to AppwriteException so isPublishable()
// suppresses expected client errors (permission denied, query timeout) from Sentry.
if ($th instanceof AuthorizationException) {
$th = new AppwriteException(AppwriteException::USER_UNAUTHORIZED, previous: $th);
} elseif ($th instanceof TimeoutException) {
$th = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $th);
}
logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization);
// Handle SQL error code is 'HY000'
@@ -1046,79 +933,56 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
Console::error('[Error] Code: ' . $response['data']['code']);
Console::error('[Error] Message: ' . $response['data']['message']);
}
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.response_code', $responseCode);
Span::add('realtime.subscription_mode', $subscriptionMode);
Span::add('realtime.channel_count', $channelCount);
Span::add('realtime.subscription_count', $subscriptionCount);
Span::add('realtime.outbound_bytes', $outboundBytes);
Span::add('realtime.responseCode', $responseCode);
Span::add('realtime.subscriptionMode', $subscriptionMode);
Span::add('realtime.channelCount', $channelCount);
Span::add('realtime.subscriptionCount', $subscriptionCount);
Span::add('realtime.outboundBytes', $outboundBytes);
if (!empty($project?->getId())) {
Span::add('project.id', $project->getId());
Span::add('realtime.projectId', $project->getId());
}
if (!empty($logUser?->getId())) {
Span::add('user.id', $logUser->getId());
Span::add('realtime.userId', $logUser->getId());
}
Span::current()?->finish();
}
});
$server->onMessage(function (int $connection, string $message) use ($container, $server, $realtime, $containerId, $register, $presenceState, $messageDispatcher) {
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId, $register) {
$project = null;
$authorization = null;
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
$rawSize = \strlen($message);
$messageType = 'invalid';
$subscriptionDelta = 0;
$subscriptionsRequested = 0;
$subscriptionsRemoved = 0;
$outboundBytes = 0;
$responseCode = 200;
$success = false;
Span::init('realtime.message');
Span::add('realtime.connection.id', $connection);
Span::add('realtime.inbound_bytes', $rawSize);
Span::add('realtime.container.id', $containerId);
Span::add('realtime.connectionId', $connection);
Span::add('realtime.inboundBytes', $rawSize);
Span::add('realtime.containerId', $containerId);
try {
$response = new Response(new SwooleResponse());
// Build a fresh Authorization per message. The connection-scoped instance is shared
// across coroutines, and `Authorization::skip()` toggles instance state — concurrent
// messages on the same connection (e.g. `authentication` + `presence` sent back-to-back)
// would interleave skip/restore and leak permission checks into supposedly-skipped lookups.
$authorization = new Authorization();
$connectionAuthorization = $realtime->connections[$connection]['authorization'] ?? null;
if ($connectionAuthorization !== null) {
foreach ($connectionAuthorization->getRoles() as $role) {
$authorization->addRole($role);
}
}
$connectionRoles = $realtime->connections[$connection]['roles'] ?? [];
foreach ($connectionRoles as $role) {
if ($authorization->hasRole($role)) {
continue;
}
$authorization->addRole($role);
// Get authorization from connection (stored during onOpen)
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
if ($authorization === null) {
$authorization = new Authorization();
}
$database = getConsoleDB();
$database->setAuthorization($authorization);
if (!empty($projectId) && $projectId !== 'console') {
// Negative-cache race: if any prior code path queried projects:$projectId
// before this project existed (e.g. a router probe during connection
// setup), the Database's shared cache may hold an empty result. Try the
// cached read first, and only purge/retry when the first lookup reports
// not-found so the shared cache remains effective for normal traffic.
try {
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
} catch (AppwriteException $e) {
if ($e->getCode() !== 404) {
throw $e;
}
$database->purgeCachedDocument('projects', $projectId);
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
}
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
$database = getProjectDB($project);
$database->setAuthorization($authorization);
@@ -1126,10 +990,6 @@ $server->onMessage(function (int $connection, string $message) use ($container,
$project = null;
}
if ($project !== null) {
checkForProjectUsage($project);
}
/*
* Abuse Check
*
@@ -1148,7 +1008,6 @@ $server->onMessage(function (int $connection, string $message) use ($container,
}
// Record realtime inbound bytes for this project
// not making this a part of the dispatcher as we need to get the inbound bytes as well even if we dont enter the dispatcher
if ($project !== null && !$project->isEmpty()) {
triggerStats([
METRIC_REALTIME_INBOUND => $rawSize,
@@ -1167,54 +1026,300 @@ $server->onMessage(function (int $connection, string $message) use ($container,
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
}
// Child of the global container: per-message values like $connection and $project
// live on this scope so concurrent message coroutines don't clobber each other,
// while globally-registered services (pools, ...) remain reachable via the parent.
$messageContainer = new Container($container);
$messageContainer->set('connectionId', fn () => $connection);
$messageContainer->set('server', fn () => $server);
$messageContainer->set('realtime', fn () => $realtime);
$messageContainer->set('register', fn () => $register);
$messageContainer->set('response', fn () => $response);
$messageContainer->set('presenceState', fn () => $presenceState);
$messageContainer->set('database', fn () => $database);
$messageContainer->set('authorization', fn () => $authorization);
$messageContainer->set('project', fn () => $project);
$messageContainer->set('projectId', fn () => $projectId);
$messageContainer->set('queueForEvents', fn () => getQueueForEvents());
$messageContainer->set('queueForRealtime', fn () => getQueueForRealtime());
$responsePayload = $messageDispatcher->dispatch($messageContainer, $message);
if ($responsePayload !== null) {
$responseJson = json_encode($responsePayload);
if ($responseJson === false) {
throw new \RuntimeException(
'Failed to encode realtime response payload: ' . json_last_error_msg()
);
}
$server->send([$connection], $responseJson);
$bytes = \strlen($responseJson);
$outboundBytes += $bytes;
if ($project !== null && !$project->isEmpty()) {
triggerStats([METRIC_REALTIME_OUTBOUND => $bytes], $project->getId());
}
// Ping does not require project context; other messages do (e.g. after unsubscribe during auth)
if (empty($projectId) && ($message['type'] ?? '') !== 'ping') {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing project context. Reconnect to the project first.');
}
switch ($message['type']) {
case 'ping':
$pongPayloadJson = json_encode([
'type' => 'pong'
]);
$server->send([$connection], $pongPayloadJson);
$outboundBytes += \strlen($pongPayloadJson);
if ($project !== null && !$project->isEmpty()) {
$pongOutboundBytes = \strlen($pongPayloadJson);
if ($pongOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $pongOutboundBytes,
], $project->getId());
}
}
break;
case 'authentication':
if (!array_key_exists('session', $message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$store = new Store();
$store->decode($message['data']['session']);
/** @var User $user */
$user = $database->getDocument('users', $store->getProperty('id', ''));
/**
* TODO:
* Moving forward, we should try to use our dependency injection container
* to inject the proof for token.
* This way we will have one source of truth for the proof for token.
*/
$proofForToken = new Token();
$proofForToken->setHash(new Sha());
if (
empty($user->getId()) // Check a document has been found in the DB
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken) // Validate user has valid login token
) {
// cookie not valid
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
}
$roles = $user->getRoles($database->getAuthorization());
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
// Capture the pre-auth userId so we can rebind any account channels
// that were stored under it (e.g. guest who subscribed to `account`
// and now authenticates). unsubscribe() below clears the connection
// entry, so we must read it first.
$previousUserId = $realtime->connections[$connection]['userId'] ?? '';
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
$meta = $realtime->getSubscriptionMetadata($connection);
$realtime->unsubscribe($connection);
if (!empty($projectId)) {
foreach ($meta as $subscriptionId => $subscription) {
$queries = Query::parseQueries($subscription['queries'] ?? []);
$channels = Realtime::rebindAccountChannels(
$subscription['channels'] ?? [],
$previousUserId,
$user->getId()
);
$realtime->subscribe(
$projectId,
$connection,
$subscriptionId,
$roles,
$channels,
$queries,
$user->getId()
);
}
}
if ($authorization !== null) {
$realtime->connections[$connection]['authorization'] = $authorization;
}
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
if ($subscriptionDelta !== 0) {
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
}
$user = $response->output($user, Response::MODEL_ACCOUNT);
$authResponsePayloadJson = json_encode([
'type' => 'response',
'data' => [
'to' => 'authentication',
'success' => true,
'user' => $user
]
]);
$server->send([$connection], $authResponsePayloadJson);
$outboundBytes += \strlen($authResponsePayloadJson);
if ($project !== null && !$project->isEmpty()) {
$authOutboundBytes = \strlen($authResponsePayloadJson);
if ($authOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $authOutboundBytes,
], $project->getId());
}
}
break;
case 'subscribe':
/**
* Message based upsertion of a subscription
* If subscriptionId is given then it will match subId of the connection and update the subscription with channels and queries
* If non-existing subid is given or not given a new subid will be generated
* Similar to what we have now -> two subscribe() block with same channels and queries still two different subscriptions
*
* structure of the payload -> array of maps
* 'data' : [subscriptionId:"" , channels:[] , queries:[]]
*/
if (!is_array($message['data']) || !array_is_list($message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$roles = $realtime->connections[$connection]['roles'] ?? [Role::guests()->toString()];
$userId = $realtime->connections[$connection]['userId'] ?? '';
// bulk validation + parsing before subscribing
$parsedPayloads = [];
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
foreach ($message['data'] as $payload) {
if (!\is_array($payload)) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each subscribe payload must be an object.');
}
if (!array_key_exists('channels', $payload)) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not present in payload.');
}
if (!is_array($payload['channels']) || !array_is_list($payload['channels'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not a valid array.');
}
// registering the queries if not present and check in the same payload later on
if (!array_key_exists('queries', $payload)) {
$payload['queries'] = [];
}
if (!is_array($payload['queries']) || !array_is_list($payload['queries'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'queries is not a valid array.');
}
$subscriptionId = \array_key_exists('subscriptionId', $payload)
? $payload['subscriptionId']
: ID::unique();
try {
$convertedQueries = Realtime::convertQueries($payload['queries']);
} catch (QueryException $e) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Invalid query: ' . $e->getMessage());
}
$convertedChannels = \array_keys(Realtime::convertChannels($payload['channels'], $userId));
$parsedPayloads[] = [
'subscriptionId' => $subscriptionId,
'channels' => $payload['channels'],
'convertedChannels' => $convertedChannels,
'queries' => $convertedQueries,
];
}
foreach ($parsedPayloads as $parsedPayload) {
$subscriptionId = $parsedPayload['subscriptionId'];
$channels = $parsedPayload['convertedChannels'];
$queries = $parsedPayload['queries'];
$realtime->subscribe($projectId, $connection, $subscriptionId, $roles, $channels, $queries);
}
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
$subscriptionsRequested = \count($parsedPayloads);
if ($subscriptionDelta !== 0) {
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
}
$responsePayload = json_encode([
'type' => 'response',
'data' => [
'to' => 'subscribe',
'success' => true,
'subscriptions' => \array_map(function (array $parsedPayload) {
return [
'subscriptionId' => $parsedPayload['subscriptionId'],
'channels' => $parsedPayload['convertedChannels'],
'queries' => \array_map(fn ($q) => $q->toString(), $parsedPayload['queries']),
];
}, $parsedPayloads),
]
]);
$server->send([$connection], $responsePayload);
$outboundBytes += \strlen($responsePayload);
if ($project !== null && !$project->isEmpty()) {
$subscribeOutboundBytes = \strlen($responsePayload);
if ($subscribeOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $subscribeOutboundBytes,
], $project->getId());
}
}
break;
case 'unsubscribe':
if (!\is_array($message['data']) || !\array_is_list($message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
// Validate every payload before executing any removal so an invalid entry
// later in the batch does not leave earlier entries half-applied on the server.
$validatedIds = [];
foreach ($message['data'] as $payload) {
if (
!\is_array($payload)
|| !\array_key_exists('subscriptionId', $payload)
|| !\is_string($payload['subscriptionId'])
|| $payload['subscriptionId'] === ''
) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each unsubscribe payload must include a non-empty subscriptionId.');
}
$validatedIds[] = $payload['subscriptionId'];
}
$unsubscribeResults = [];
foreach ($validatedIds as $subscriptionId) {
$wasRemoved = $realtime->unsubscribeSubscription($connection, $subscriptionId);
$unsubscribeResults[] = [
'subscriptionId' => $subscriptionId,
'removed' => $wasRemoved,
];
}
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
$subscriptionsRequested = \count($validatedIds);
$subscriptionsRemoved = \count(\array_filter($unsubscribeResults, fn (array $item) => $item['removed']));
if ($subscriptionDelta !== 0) {
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
}
$unsubscribeResponsePayload = json_encode([
'type' => 'response',
'data' => [
'to' => 'unsubscribe',
'success' => true,
'subscriptions' => $unsubscribeResults,
],
]);
$server->send([$connection], $unsubscribeResponsePayload);
$outboundBytes += \strlen($unsubscribeResponsePayload);
if ($project !== null && !$project->isEmpty()) {
$unsubscribeOutboundBytes = \strlen($unsubscribeResponsePayload);
if ($unsubscribeOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $unsubscribeOutboundBytes,
], $project->getId());
}
}
break;
default:
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
}
$success = true;
} catch (Throwable $th) {
Span::error($th);
// Convert known Utopia DB exceptions to AppwriteException so isPublishable()
// suppresses expected client errors (permission denied, query timeout) from Sentry.
if ($th instanceof AuthorizationException) {
$th = new AppwriteException(AppwriteException::USER_UNAUTHORIZED, previous: $th);
} elseif ($th instanceof TimeoutException) {
$th = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $th);
}
logError($th, 'realtimeMessage', project: $project, authorization: $authorization);
$code = $th->getCode();
if (!is_int($code)) {
@@ -1244,25 +1349,30 @@ $server->onMessage(function (int $connection, string $message) use ($container,
if ($th->getCode() === 1008) {
$server->close($connection, $th->getCode());
}
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.response_code', $responseCode);
Span::add('realtime.outbound_bytes', $outboundBytes);
Span::add('project.id', $project?->getId() ?? $projectId);
Span::add('user.id', $realtime->connections[$connection]['userId'] ?? null);
Span::add('realtime.message_type', $messageType);
Span::add('realtime.responseCode', $responseCode);
Span::add('realtime.subscriptionDelta', $subscriptionDelta);
Span::add('realtime.subscriptionsRequested', $subscriptionsRequested);
Span::add('realtime.subscriptionsRemoved', $subscriptionsRemoved);
Span::add('realtime.subscribe.subscriptionsCount', $subscriptionsRequested);
Span::add('realtime.outboundBytes', $outboundBytes);
Span::add('realtime.projectId', $project?->getId() ?? $projectId);
Span::add('realtime.userId', $realtime->connections[$connection]['userId'] ?? null);
Span::add('realtime.messageType', $messageType);
Span::current()?->finish();
}
});
$server->onClose(function (int $connection) use ($realtime, $stats, $register, $container, $presenceState) {
$server->onClose(function (int $connection) use ($realtime, $stats, $register) {
$projectId = null;
$userId = null;
$subscriptionsBeforeClose = 0;
$success = false;
Span::init('realtime.close');
Span::add('realtime.connection.id', $connection);
Span::add('realtime.connectionId', $connection);
if (array_key_exists($connection, $realtime->connections)) {
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
@@ -1280,100 +1390,6 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register, $
}
$projectId = $realtime->connections[$connection]['projectId'];
/** @var array<string, Document> $presencesById */
$presencesById = $realtime->connections[$connection]['presences'] ?? [];
if (
!empty($presencesById)
&& $projectId !== 'console'
) {
go(function () use ($presencesById, $projectId, $userId, $container, $presenceState): void {
// Fresh span: the parent realtime.close span finishes before this coroutine
Span::init('realtime.close.presenceCleanup');
Span::add('realtime.projectId', $projectId);
Span::add('realtime.presenceCount', \count($presencesById));
try {
$dbForPlatform = getConsoleDB();
$project = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if ($project->isEmpty()) {
return;
}
$presenceIds = \array_keys($presencesById);
$presences = \array_values($presencesById);
$dbForProject = getProjectDB($project);
$user = new User([]);
if (!empty($userId)) {
try {
$fetched = $dbForProject->getAuthorization()->skip(
fn () => $dbForProject->getDocument('users', $userId)
);
if (!$fetched->isEmpty()) {
$user = new User($fetched->getArrayCopy());
}
} catch (Throwable) {
// Fall back to empty User if lookup fails.
}
}
/** @var UsagePublisher $publisherForUsage */
$publisherForUsage = $container->get('publisherForUsage');
/** @var array<string, true> $deletedIds */
$deletedIds = [];
try {
$deletionCount = $dbForProject->getAuthorization()->skip(
function () use ($dbForProject, $presenceIds, &$deletedIds): int {
return $dbForProject->deleteDocuments(
'presenceLogs',
[Query::equal('$id', $presenceIds)],
onNext: function (Document $deleted) use (&$deletedIds): void {
$deletedIds[$deleted->getId()] = true;
},
);
}
);
$presenceState->triggerUsage($publisherForUsage, $project, -$deletionCount);
} catch (Throwable $th) {
Span::error($th);
logError($th, 'realtimeOnClosePresenceDeletion', tags: [
'projectId' => $projectId,
'presences' => \count($presences)
]);
}
$queueForEvents = getQueueForEvents();
$queueForRealtime = getQueueForRealtime();
foreach ($presences as $presence) {
if (!isset($deletedIds[$presence->getId()])) {
continue;
}
try {
$presenceState->triggerEvent(
$queueForEvents,
$queueForRealtime,
$project,
$user,
'presences.[presenceId].delete',
$presence,
);
} catch (Throwable) {
// Swallow errors to avoid breaking disconnect cleanup
}
}
} catch (Throwable $th) {
Span::error($th);
logError($th, 'realtimeOnClosePresenceCleanup', tags: [
'projectId' => $projectId,
]);
} finally {
Span::current()?->finish();
}
});
}
triggerStats([
METRIC_REALTIME_CONNECTIONS => -1,
@@ -1395,12 +1411,12 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register, $
Span::add('realtime.success', $success);
if (!empty($projectId)) {
Span::add('project.id', $projectId);
Span::add('realtime.projectId', $projectId);
}
if (!empty($userId)) {
Span::add('user.id', $userId);
Span::add('realtime.userId', $userId);
}
Span::add('realtime.subscriptions_before_close', $subscriptionsBeforeClose);
Span::add('realtime.subscriptionsBeforeClose', $subscriptionsBeforeClose);
Span::current()?->finish();
}
-14
View File
@@ -881,13 +881,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -916,13 +909,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
+1 -9
View File
@@ -16,7 +16,6 @@ use Utopia\Pools\Group;
use Utopia\Queue\Adapter\Swoole;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Server;
use Utopia\Span\Span;
use Utopia\System\System;
Runtime::enableCoroutine();
@@ -92,13 +91,8 @@ $adapter = new Swoole(
$worker = new Server($adapter, $container);
try {
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources, $queueName) {
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
$registerWorkerMessageResources($worker->getContainer());
Span::init("worker.{$queueName}");
});
$worker->shutdown()->action(function () {
Span::current()?->finish();
});
$container->set('bus', function ($register) use ($worker) {
@@ -126,8 +120,6 @@ $worker
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
Span::error($error);
if ($logger) {
$log->setNamespace('appwrite-worker');
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
exec php /usr/src/code/app/worker.php notifications "$@"
+16 -17
View File
@@ -49,44 +49,43 @@
"ext-openssl": "*",
"ext-zlib": "*",
"ext-sockets": "*",
"appwrite/php-runtimes": "0.20.*",
"appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.3.*",
"utopia-php/abuse": "1.2.*",
"utopia-php/agents": "1.2.*",
"utopia-php/analytics": "0.15.*",
"utopia-php/audit": "^2.4",
"utopia-php/audit": "2.2.*",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "^3.0",
"utopia-php/cache": "1.0.*",
"utopia-php/cli": "0.23.*",
"utopia-php/compression": "0.1.*",
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
"utopia-php/database": "5.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "^2.1",
"utopia-php/emails": "0.7.*",
"utopia-php/dns": "1.7.*",
"utopia-php/domains": "1.*",
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.6.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/http": "2.0.0-rc3",
"utopia-php/fetch": "^1.1",
"utopia-php/http": "0.34.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/validators": "0.2.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/lock": "0.2.*",
"utopia-php/logger": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.*",
"utopia-php/platform": "1.0.0-rc3",
"utopia-php/migration": "1.9.*",
"utopia-php/platform": "0.13.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.18.*",
"utopia-php/servers": "0.4.*",
"utopia-php/queue": "0.17.*",
"utopia-php/servers": "0.3.*",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "2.*",
"utopia-php/system": "0.10.*",
"utopia-php/telemetry": "0.2.*",
"utopia-php/vcs": "4.*",
"utopia-php/vcs": "3.*",
"utopia-php/websocket": "1.0.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
@@ -94,7 +93,7 @@
"chillerlan/php-qrcode": "4.3.*",
"adhocore/jwt": "1.1.*",
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "15.32.*",
"webonyx/graphql-php": "15.31.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*"
},
Generated
+367 -473
View File
File diff suppressed because it is too large Load Diff
+43 -16
View File
@@ -255,7 +255,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:8
image: appwrite/console:7.8.45
restart: unless-stopped
networks:
- appwrite
@@ -806,6 +806,47 @@ services:
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-notifications:
entrypoint: worker-notifications
<<: [*x-logging, *x-build]
container_name: appwrite-worker-notifications
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- maildev
- ${_APP_DB_HOST:-mariadb}
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_POOL_ADAPTER
- _APP_OPENSSL_KEY_V1
- _APP_SYSTEM_EMAIL_NAME
- _APP_SYSTEM_EMAIL_ADDRESS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
- _APP_SMTP_USERNAME
- _APP_SMTP_PASSWORD
- _APP_LOGGING_CONFIG
- _APP_DOMAIN
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-messaging:
entrypoint: worker-messaging
<<: [*x-logging, *x-build]
@@ -1114,13 +1155,6 @@ services:
- _APP_WORKER_PER_CORE
- _APP_POOL_ADAPTER
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1152,13 +1186,6 @@ services:
- _APP_WORKER_PER_CORE
- _APP_POOL_ADAPTER
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1492,4 +1519,4 @@ volumes:
appwrite-sites:
appwrite-builds:
appwrite-config:
appwrite-models:
appwrite-models:
-1
View File
@@ -1 +0,0 @@
Delete an analyzer report by its unique ID. Nested insights and CTA metadata are removed asynchronously by the deletes worker.
-1
View File
@@ -1 +0,0 @@
Get an insight by its unique ID, scoped to its parent report.
-1
View File
@@ -1 +0,0 @@
Get an analyzer report by its unique ID. The response includes the report's metadata and the nested insights it produced.
-1
View File
@@ -1 +0,0 @@
List the insights produced under a single analyzer report. You can use the query params to filter your results further.
-1
View File
@@ -1 +0,0 @@
Get a list of all the project's analyzer reports. You can use the query params to filter your results.
@@ -1 +0,0 @@
Create a bigint attribute. Optionally, minimum and maximum values can be provided.
@@ -1 +0,0 @@
Update a bigint attribute. Changing the `default` value will not update already existing documents.
-1
View File
@@ -1 +0,0 @@
Delete a presence log by its unique ID.
-1
View File
@@ -1 +0,0 @@
Get presence usage metrics, including the current total of online users and historical online user counts for the selected time range.
-1
View File
@@ -1 +0,0 @@
Get a presence log by its unique ID. Entries whose `expiresAt` is in the past are treated as not found.
-1
View File
@@ -1 +0,0 @@
List presence logs. Expired entries are filtered out automatically.
-1
View File
@@ -1 +0,0 @@
Update a presence log by its unique ID. Using the patch method you can pass only specific fields that will get updated.
-1
View File
@@ -1 +0,0 @@
Create or update a presence log by its user ID.
+1
View File
@@ -0,0 +1 @@
Create a new JWT token. This token can be used to authenticate users with custom scopes and expiration time.
@@ -0,0 +1 @@
Send a test email to verify SMTP configuration.
+1
View File
@@ -0,0 +1 @@
Create a new project. You can create a maximum of 100 projects per account.
@@ -0,0 +1 @@
Reset a custom email template to its default value. This endpoint removes any custom content and restores the template to its original state.
+1
View File
@@ -0,0 +1 @@
Delete a project by its unique ID.
@@ -0,0 +1 @@
Get a custom email template for the specified locale and type. This endpoint returns the template content, subject, and other configuration details.
+1
View File
@@ -0,0 +1 @@
Get a project by its unique ID. This endpoint allows you to retrieve the project's details, including its name, description, team, region, and other metadata.
@@ -0,0 +1 @@
Update the status of a specific authentication method. Use this endpoint to enable or disable different authentication methods such as email, magic urls or sms in your project.
@@ -0,0 +1 @@
Update a custom email template for the specified locale and type. Use this endpoint to modify the content of your email templates.
@@ -0,0 +1 @@
Update the list of mock phone numbers for testing. Use these numbers to bypass SMS verification in development.
@@ -0,0 +1 @@
Update the OAuth2 provider configurations. Use this endpoint to set up or update the OAuth2 provider credentials or enable/disable providers.
+1
View File
@@ -0,0 +1 @@
Update the SMTP configuration for your project. Use this endpoint to configure your project's SMTP provider with your custom settings for sending transactional emails.
+1
View File
@@ -0,0 +1 @@
Update a project by its unique ID.
@@ -1 +0,0 @@
Create a bigint column. Optionally, minimum and maximum values can be provided.
@@ -1 +0,0 @@
Update a bigint column. Changing the `default` value will not update already existing rows.
@@ -1 +1 @@
Get a list of branches from a GitHub repository in your installation. This endpoint supports filtering by a search term and pagination using query strings such as `Query.limit()`, `Query.offset()`, `Query.cursorAfter()`, and `Query.cursorBefore()`. It returns branch names along with the total number of matches. The GitHub installation must be properly configured and have access to the requested repository for this endpoint to work.
Get a list of all branches from a GitHub repository in your installation. This endpoint returns the names of all branches in the repository and their total count. The GitHub installation must be properly configured and have access to the requested repository for this endpoint to work.
-116
View File
@@ -1,116 +0,0 @@
## Getting Started
Before you begin, create an Appwrite project and add a Unity platform in your Appwrite Console.
This SDK requires the following Unity packages and libraries:
- [**UniTask**](https://github.com/Cysharp/UniTask): For async/await support in Unity.
- [**NativeWebSocket**](https://github.com/endel/NativeWebSocket): For WebSocket realtime subscriptions.
- **System.Text.Json**: For JSON serialization, provided as a DLL in the project.
After installing the SDK, open **Appwrite → Setup Assistant** in Unity and install the required dependencies.
### Configure the SDK
Create an Appwrite configuration using the **QuickStart** window in the **Appwrite Setup Assistant**, or through **Appwrite → Create Configuration**.
### Using AppwriteManager
```csharp
[SerializeField] private AppwriteConfig config;
private AppwriteManager _manager;
private async UniTask ExampleWithManager()
{
_manager = AppwriteManager.Instance ?? new GameObject("AppwriteManager").AddComponent<AppwriteManager>();
_manager.SetConfig(config);
var success = await _manager.Initialize(needRealtime: true);
if (!success)
{
Debug.LogError("Failed to initialize AppwriteManager");
return;
}
var client = _manager.Client;
var pingResult = await client.Ping();
Debug.Log($"Ping result: {pingResult}");
var realtime = _manager.Realtime;
var subscription = realtime.Subscribe(
new[] { "databases.*.collections.*.documents" },
response =>
{
var eventName = response.Events != null && response.Events.Length > 0
? response.Events[0]
: "unknown";
Debug.Log($"Realtime event: {eventName}");
}
);
// Keep a reference to close the subscription when your MonoBehaviour is destroyed.
// subscription.Close();
}
```
### Using Client directly
```csharp
private async UniTask ExampleWithDirectClient()
{
var client = Client.From(
projectId: "<PROJECT_ID>",
endpoint: "https://<REGION>.cloud.appwrite.io/v1",
endpointRealtime: "wss://<REGION>.cloud.appwrite.io/v1");
var pingResult = await client.Ping();
Debug.Log($"Direct client ping: {pingResult}");
}
```
You can also create authenticated clients with `Client.FromSession`, `Client.FromDevKey`, or `Client.FromImpersonation` when those authentication flows are needed.
### Error handling
```csharp
try
{
var result = await client.Ping();
}
catch (AppwriteException ex)
{
Debug.LogError($"Appwrite Error: {ex.Message}");
Debug.LogError($"Status Code: {ex.Code}");
Debug.LogError($"Response: {ex.Response}");
}
```
## Preparing Models for Databases API
When working with the Databases API in Unity, models should be prepared for serialization using the System.Text.Json library. System.Text.Json uses CLR property names by default unless a naming policy is configured. If your project or SDK configuration serializes property names differently from your Appwrite collection attributes, this can cause errors due to mismatches between serialized property names and actual attribute names in your collection.
To avoid this, add the `JsonPropertyName` attribute to each property in your model class to match the attribute name in Appwrite:
```csharp
using System.Text.Json.Serialization;
public class TestModel
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("release_date")]
public System.DateTime ReleaseDate { get; set; }
}
```
The `JsonPropertyName` attribute ensures your data object is serialized with the correct attribute names for Appwrite databases.
### Learn more
You can use the following resources to learn more and get help:
- 🚀 [Getting Started Tutorial](https://appwrite.io/docs/getting-started-for-client)
- 📜 [Appwrite Docs](https://appwrite.io/docs)
- 💬 [Discord Community](https://appwrite.io/discord)
- 🧰 [Appwrite SDK Generator](https://github.com/appwrite/sdk-generator)
-3
View File
@@ -1,3 +0,0 @@
The Advisor service provides read access to analyzer reports and their nested insights for a project.
Use the reports endpoints to list and fetch analyzer runs, then use the insights endpoints to inspect individual findings attached to a report.
+1 -1
View File
@@ -37,8 +37,8 @@
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
<directory>./tests/e2e/Services/Messaging</directory>
<directory>./tests/e2e/Services/Migrations</directory>
<directory>./tests/e2e/Services/Notifications</directory>
<directory>./tests/e2e/Services/Project</directory>
<directory>./tests/e2e/Services/Advisor</directory>
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
-83
View File
@@ -1,83 +0,0 @@
<?php
namespace Appwrite\Advisor\Validator;
use Utopia\Validator;
class CTAs extends Validator
{
public const MAX_COUNT_DEFAULT = 16;
protected string $message = 'Value must be an array of CTA descriptors. Each entry must define `label`, `service`, `method`, and an optional `params` object.';
protected array $allowedServices;
protected array $allowedMethods;
public function __construct(
protected int $maxCount = self::MAX_COUNT_DEFAULT,
?array $allowedServices = null,
?array $allowedMethods = null,
) {
$this->allowedServices = $allowedServices ?? ADVISOR_CTA_SERVICES;
$this->allowedMethods = $allowedMethods ?? ADVISOR_CTA_METHODS;
}
public function getDescription(): string
{
return $this->message;
}
public function isArray(): bool
{
return true;
}
public function getType(): string
{
return self::TYPE_ARRAY;
}
public function isValid($value): bool
{
if (!\is_array($value)) {
return false;
}
if (\count($value) > $this->maxCount) {
$this->message = "A maximum of {$this->maxCount} CTAs are allowed per insight.";
return false;
}
foreach ($value as $entry) {
if (!\is_array($entry)) {
return false;
}
$maxLengths = ['label' => 256, 'service' => 64, 'method' => 64];
foreach ($maxLengths as $required => $maxLength) {
if (!isset($entry[$required]) || !\is_string($entry[$required]) || $entry[$required] === '') {
return false;
}
if (\strlen($entry[$required]) > $maxLength) {
$this->message = "CTA `{$required}` must not exceed {$maxLength} characters.";
return false;
}
}
if (!empty($this->allowedServices) && !\in_array($entry['service'], $this->allowedServices, true)) {
$this->message = "CTA `service` must be one of: " . \implode(', ', $this->allowedServices) . '.';
return false;
}
if (!empty($this->allowedMethods) && !\in_array($entry['method'], $this->allowedMethods, true)) {
$this->message = "CTA `method` must be one of: " . \implode(', ', $this->allowedMethods) . '.';
return false;
}
if (isset($entry['params']) && !\is_array($entry['params']) && !\is_object($entry['params'])) {
return false;
}
}
return true;
}
}
+3 -3
View File
@@ -122,9 +122,9 @@ class Key
$secret = $key;
}
$role = User::ROLE_KEYS;
$role = User::ROLE_APPS;
$roles = Config::getParam('roles', []);
$scopes = $roles[User::ROLE_KEYS]['scopes'] ?? [];
$scopes = $roles[User::ROLE_APPS]['scopes'] ?? [];
$expired = false;
$guestKey = new Key(
@@ -270,7 +270,7 @@ class Key
$name = $key->getAttribute('name', 'UNKNOWN');
$role = User::ROLE_KEYS;
$role = User::ROLE_APPS;
$scopes = $key->getAttribute('scopes', []);
+3 -53
View File
@@ -55,7 +55,7 @@ class Google extends OAuth2
'state' => \json_encode($this->state),
'response_type' => 'code',
'access_type' => 'offline',
'prompt' => $this->getPrompt()
'prompt' => 'consent'
]);
}
@@ -72,7 +72,7 @@ class Google extends OAuth2
'https://oauth2.googleapis.com/token?' . \http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->getClientSecret(),
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'scope' => null,
'grant_type' => 'authorization_code'
@@ -95,7 +95,7 @@ class Google extends OAuth2
'https://oauth2.googleapis.com/token?' . \http_build_query([
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->getClientSecret(),
'client_secret' => $this->appSecret,
'grant_type' => 'refresh_token'
])
), true);
@@ -177,54 +177,4 @@ class Google extends OAuth2
return $this->user;
}
/**
* Extracts the Client Secret from the JSON stored in appSecret
*
* @return string
*/
protected function getClientSecret(): string
{
$secret = $this->getAppSecret();
return $secret['clientSecret'] ?? $this->appSecret;
}
/**
* Extracts the prompt values from the JSON stored in appSecret
*
* @return string
*/
protected function getPrompt(): string
{
$secret = $this->getAppSecret();
$prompt = $secret['prompt'] ?? [];
if (empty($prompt)) {
$prompt = ['consent'];
}
return \implode(' ', $prompt);
}
/**
* Decode the JSON stored in appSecret.
* Falls back to treating the raw string as the client secret for backwards compatibility.
*
* @return array
*/
protected function getAppSecret(): array
{
try {
$secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR);
} catch (\Throwable $th) {
return ['clientSecret' => $this->appSecret];
}
if (!\is_array($secret)) {
return ['clientSecret' => $this->appSecret];
}
return $secret;
}
}
+14 -6
View File
@@ -8,6 +8,7 @@ use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Span\Span;
use Utopia\System\System;
class Log extends Listener
{
@@ -33,13 +34,20 @@ class Log extends Listener
{
$project = new Document($event->project);
$execution = new Document($event->execution);
if ($execution->getAttribute('resourceType', '') === 'functions') {
Span::add('project.id', $project->getId());
Span::add('function.id', $execution->getAttribute('resourceId', ''));
Span::add('execution.id', $execution->getId());
Span::add('deployment.id', $execution->getAttribute('deploymentId', ''));
Span::add('execution.status', $execution->getAttribute('status', ''));
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$resourceId = $execution->getAttribute('resourceId', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
Span::init('execution.trace.v1_executions_enqueue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $resourceId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('status', $execution->getAttribute('status', ''));
Span::current()?->finish();
}
}
$publisherForExecutions->enqueue(new ExecutionMessage(
+30 -28
View File
@@ -4,14 +4,14 @@ namespace Appwrite\Bus\Listeners;
use Appwrite\Auth\MFA\Type;
use Appwrite\Bus\Events\SessionCreated;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Mail;
use Appwrite\Template\Template;
use Utopia\Bus\Listener;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Locale\Locale;
use Utopia\Queue\Publisher;
use Utopia\Storage\Validator\FileName;
use Utopia\System\System;
@@ -31,14 +31,14 @@ class Mails extends Listener
{
$this
->desc('Sends session alert emails')
->inject('publisherForMails')
->inject('publisher')
->inject('locale')
->inject('platform')
->inject('dbForProject')
->callback($this->handle(...));
}
public function handle(SessionCreated $event, MailPublisher $publisherForMails, Locale $locale, array $platform, Database $dbForProject): void
public function handle(SessionCreated $event, Publisher $publisher, Locale $locale, array $platform, Database $dbForProject): void
{
$project = new Document($event->project);
@@ -124,32 +124,34 @@ class Mails extends Listener
];
}
$smtpConfig = [];
$queueForMails = new Mail($publisher);
if ($smtp['enabled'] ?? false) {
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '', // Includes backwards compatibility
'replyToName' => $customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '',
'senderEmail' => $customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM),
'senderName' => $customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'),
];
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '')
->setSmtpReplyToEmail($customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '') // Includes backwards compatibility
->setSmtpReplyToName($customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '')
->setSmtpSenderEmail($customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM))
->setSmtpSenderName($customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
}
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $event->user['email'],
subject: $subject,
bodyTemplate: __DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl',
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $isBranded ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$queueForMails
->setProject($project)
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl')
->appendVariables($emailVariables)
->setRecipient($event->user['email']);
if ($isBranded) {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
namespace Appwrite\Event;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\System\System;
class Build extends Event
{
protected string $type = '';
protected ?Document $resource = null;
protected ?Document $deployment = null;
protected ?Document $template = null;
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
->setClass(System::getEnv('_APP_BUILDS_CLASS_NAME', Event::BUILDS_CLASS_NAME));
}
/**
* Sets template for the build event.
*
* @param Document $template
* @return self
*/
public function setTemplate(Document $template): self
{
$this->template = $template;
return $this;
}
/**
* Sets resource document for the build event.
*
* @param Document $resource
* @return self
*/
public function setResource(Document $resource): self
{
$this->resource = $resource;
return $this;
}
/**
* Returns set resource document for the build event.
*
* @return null|Document
*/
public function getResource(): ?Document
{
return $this->resource;
}
/**
* Sets deployment for the build event.
*
* @param Document $deployment
* @return self
*/
public function setDeployment(Document $deployment): self
{
$this->deployment = $deployment;
return $this;
}
/**
* Returns set deployment for the build event.
*
* @return null|Document
*/
public function getDeployment(): ?Document
{
return $this->deployment;
}
/**
* Sets type for the build event.
*
* @param string $type Can be `BUILD_TYPE_DEPLOYMENT` or `BUILD_TYPE_RETRY`.
* @return self
*/
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
/**
* Returns set type for the function event.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Prepare payload for queue.
*
* @return array
*/
protected function preparePayload(): array
{
$platform = $this->platform;
if (empty($platform)) {
$platform = Config::getParam('platform', []);
}
return [
'project' => $this->project,
'resource' => $this->resource,
'deployment' => $this->deployment,
'type' => $this->type,
'template' => $this->template,
'platform' => $platform,
];
}
/**
* Resets event.
*
* @return self
*/
public function reset(): self
{
$this->type = '';
$this->resource = null;
$this->deployment = null;
$this->template = null;
$this->platform = [];
parent::reset();
return $this;
}
}
+3
View File
@@ -21,6 +21,9 @@ class Event
public const MAILS_QUEUE_NAME = 'v1-mails';
public const MAILS_CLASS_NAME = 'MailsV1';
public const NOTIFICATIONS_QUEUE_NAME = 'v1-notifications';
public const NOTIFICATIONS_CLASS_NAME = 'NotificationsV1';
public const FUNCTIONS_QUEUE_NAME = 'v1-functions';
public const FUNCTIONS_CLASS_NAME = 'FunctionsV1';
public const FUNCTIONS_QUEUE_TTL = 60 * 60 * 24 * 7; // 7 days
+576
View File
@@ -0,0 +1,576 @@
<?php
namespace Appwrite\Event;
use Utopia\Config\Config;
use Utopia\Queue\Publisher;
use Utopia\System\System;
class Mail extends Event
{
protected string $recipient = '';
protected string $name = '';
protected string $subject = '';
protected string $body = '';
protected string $preview = '';
protected array $smtp = [];
protected array $variables = [];
protected string $bodyTemplate = '';
protected array $attachment = [];
protected array $customMailOptions = [];
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
->setClass(System::getEnv('_APP_MAILS_CLASS_NAME', Event::MAILS_CLASS_NAME));
}
/**
* Sets subject for the mail event.
*
* @param string $subject
* @return self
*/
public function setSubject(string $subject): self
{
$this->subject = $subject;
return $this;
}
/**
* Returns subject for the mail event.
*
* @return string
*/
public function getSubject(): string
{
return $this->subject;
}
/**
* Sets recipient for the mail event.
*
* @param string $recipient
* @return self
*/
public function setRecipient(string $recipient): self
{
$this->recipient = $recipient;
return $this;
}
/**
* Returns set recipient for mail event.
*
* @return string
*/
public function getRecipient(): string
{
return $this->recipient;
}
/**
* Sets body for the mail event.
*
* @param string $body
* @return self
*/
public function setBody(string $body): self
{
$this->body = $body;
return $this;
}
/**
* Returns body for the mail event.
*
* @return string
*/
public function getBody(): string
{
return $this->body;
}
/**
* Sets preview for the mail event.
*
* @return self
*/
public function setPreview(string $preview): self
{
$this->preview = $preview;
return $this;
}
/**
* Returns preview for the mail event.
*
* @return string
*/
public function getPreview(): string
{
return $this->preview;
}
/**
* Sets name for the mail event.
*
* @param string $name
* @return self
*/
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* Returns set name for the mail event.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Sets bodyTemplate for the mail event.
*
* @param string $bodyTemplate
* @return self
*/
public function setBodyTemplate(string $bodyTemplate): self
{
$this->bodyTemplate = $bodyTemplate;
return $this;
}
/**
* Returns subject for the mail event.
*
* @return string
*/
public function getBodyTemplate(): string
{
return $this->bodyTemplate;
}
/**
* Set SMTP Host
*
* @param string $host
* @return self
*/
public function setSmtpHost(string $host): self
{
$this->smtp['host'] = $host;
return $this;
}
/**
* Set SMTP port
*
* @param int $port
* @return self
*/
public function setSmtpPort(int $port): self
{
$this->smtp['port'] = $port;
return $this;
}
/**
* Set SMTP username
*
* @param string $username
* @return self
*/
public function setSmtpUsername(string $username): self
{
$this->smtp['username'] = $username;
return $this;
}
/**
* Set SMTP password
*
* @param string $password
* @return self
*/
public function setSmtpPassword(string $password): self
{
$this->smtp['password'] = $password;
return $this;
}
/**
* Set SMTP secure
*
* @param string $secure
* @return self
*/
public function setSmtpSecure(string $secure): self
{
$this->smtp['secure'] = $secure;
return $this;
}
/**
* Set SMTP sender email
*
* @param string $senderEmail
* @return self
*/
public function setSmtpSenderEmail(string $senderEmail): self
{
$this->smtp['senderEmail'] = $senderEmail;
return $this;
}
/**
* Set SMTP sender name
*
* @param string $senderName
* @return self
*/
public function setSmtpSenderName(string $senderName): self
{
$this->smtp['senderName'] = $senderName;
return $this;
}
/**
* Set SMTP reply-to email
*
* @param string $email
* @return self
*/
public function setSmtpReplyToEmail(string $email): self
{
$this->smtp['replyToEmail'] = $email;
return $this;
}
/**
* Set SMTP reply-to name
*
* @param string $name
* @return self
*/
public function setSmtpReplyToName(string $name): self
{
$this->smtp['replyToName'] = $name;
return $this;
}
/**
* Get SMTP
*
* @return string
*/
public function getSmtpHost(): string
{
return $this->smtp['host'] ?? '';
}
/**
* Get SMTP port
*
* @return integer
*/
public function getSmtpPort(): int
{
return $this->smtp['port'] ?? 0;
}
/**
* Get SMTP username
*
* @return string
*/
public function getSmtpUsername(): string
{
return $this->smtp['username'] ?? '';
}
/**
* Get SMTP password
*
* @return string
*/
public function getSmtpPassword(): string
{
return $this->smtp['password'] ?? '';
}
/**
* Get SMTP secure
*
* @return string
*/
public function getSmtpSecure(): string
{
return $this->smtp['secure'] ?? '';
}
/**
* Get SMTP sender email
*
* @return string
*/
public function getSmtpSenderEmail(): string
{
return $this->smtp['senderEmail'] ?? '';
}
/**
* Get SMTP sender name
*
* @return string
*/
public function getSmtpSenderName(): string
{
return $this->smtp['senderName'] ?? '';
}
/**
* Get SMTP reply-to email
*
* @return string
*/
public function getSmtpReplyToEmail(): string
{
return $this->smtp['replyToEmail'] ?? '';
}
/**
* Get SMTP reply-to name
*
* @return string
*/
public function getSmtpReplyToName(): string
{
return $this->smtp['replyToName'] ?? '';
}
/**
* Get Email Variables
*
* @return array
*/
public function getVariables(): array
{
return $this->variables;
}
/**
* Set Email Variables
*
* @param array $variables
* @return self
*/
public function setVariables(array $variables): self
{
$this->variables = $variables;
return $this;
}
/**
* Append variables to the email event.
*
* @param array $variables
* @return self
*/
public function appendVariables(array $variables): self
{
$this->variables = \array_merge($this->variables, $variables);
return $this;
}
/**
* Set attachment
* @param string $content
* @param string $filename
* @param string $encoding
* @param string $type
* @return self
*/
public function setAttachment(string $content, string $filename, string $encoding = 'base64', string $type = 'plain/text')
{
$this->attachment = [
'content' => base64_encode($content),
'filename' => $filename,
'encoding' => $encoding,
'type' => $type,
];
return $this;
}
/**
* Get attachment
*
* @return array
*/
public function getAttachment(): array
{
return $this->attachment;
}
/**
* Reset attachment
*
* @return self
*/
public function resetAttachment(): self
{
$this->attachment = [];
return $this;
}
/**
* Set sender email
*
* @param string $email
* @return self
*/
public function setSenderEmail(string $email): self
{
$this->customMailOptions['senderEmail'] = $email;
return $this;
}
/**
* Get sender email
*
* @return string
*/
public function getSenderEmail(): string
{
return $this->customMailOptions['senderEmail'] ?? '';
}
/**
* Set sender name
*
* @param string $name
* @return self
*/
public function setSenderName(string $name): self
{
$this->customMailOptions['senderName'] = $name;
return $this;
}
/**
* Get sender name
*
* @return string
*/
public function getSenderName(): string
{
return $this->customMailOptions['senderName'] ?? '';
}
/**
* Set reply-to email
*
* @param string $email
* @return self
*/
public function setReplyToEmail(string $email): self
{
$this->customMailOptions['replyToEmail'] = $email;
return $this;
}
/**
* Get reply-to email
*
* @return string
*/
public function getReplyToEmail(): string
{
return $this->customMailOptions['replyToEmail'] ?? '';
}
/**
* Set reply-to name
*
* @param string $name
* @return self
*/
public function setReplyToName(string $name): self
{
$this->customMailOptions['replyToName'] = $name;
return $this;
}
/**
* Get reply-to name
*
* @return string
*/
public function getReplyToName(): string
{
return $this->customMailOptions['replyToName'] ?? '';
}
/**
* Reset
*
* @return self
*/
public function reset(): self
{
$this->project = null;
$this->recipient = '';
$this->name = '';
$this->subject = '';
$this->body = '';
$this->variables = [];
$this->bodyTemplate = '';
$this->attachment = [];
$this->customMailOptions = [];
return $this;
}
/**
* Prepare the payload for the event
*
* @return array
*/
protected function preparePayload(): array
{
$platform = $this->platform;
if (empty($platform)) {
$platform = Config::getParam('platform', []);
}
return [
'project' => $this->project,
'recipient' => $this->recipient,
'name' => $this->name,
'subject' => $this->subject,
'bodyTemplate' => $this->bodyTemplate,
'body' => $this->body,
'preview' => $this->preview,
'smtp' => $this->smtp,
'variables' => $this->variables,
'attachment' => $this->attachment,
'customMailOptions' => $this->customMailOptions,
'events' => Event::generateEvents($this->getEvent(), $this->getParams()),
'platform' => $platform,
];
}
}
-45
View File
@@ -1,45 +0,0 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Config\Config;
use Utopia\Database\Document;
final class Build extends Base
{
public function __construct(
public readonly Document $project,
public readonly Document $resource,
public readonly Document $deployment,
public readonly string $type,
public readonly ?Document $template = null,
public readonly array $platform = [],
) {
}
public function toArray(): array
{
$platform = !empty($this->platform) ? $this->platform : Config::getParam('platform', []);
return [
'project' => $this->project->getArrayCopy(),
'resource' => $this->resource->getArrayCopy(),
'deployment' => $this->deployment->getArrayCopy(),
'type' => $this->type,
'template' => $this->template?->getArrayCopy(),
'platform' => $platform,
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
resource: new Document($data['resource'] ?? []),
deployment: new Document($data['deployment'] ?? []),
type: $data['type'] ?? '',
template: !empty($data['template']) ? new Document($data['template']) : null,
platform: $data['platform'] ?? [],
);
}
}
-51
View File
@@ -1,51 +0,0 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Database extends Base
{
public function __construct(
public readonly ?Document $project = null,
public readonly ?Document $user = null,
public readonly string $type = '',
public readonly ?Document $table = null,
public readonly ?Document $row = null,
public readonly ?Document $collection = null,
public readonly ?Document $document = null,
public readonly ?Document $database = null,
public readonly array $events = [],
) {
}
public function toArray(): array
{
return [
'project' => $this->project?->getArrayCopy(),
'user' => $this->user?->getArrayCopy(),
'type' => $this->type,
'table' => $this->table?->getArrayCopy(),
'row' => $this->row?->getArrayCopy(),
'collection' => $this->collection?->getArrayCopy(),
'document' => $this->document?->getArrayCopy(),
'database' => $this->database?->getArrayCopy(),
'events' => $this->events,
];
}
public static function fromArray(array $data): static
{
return new self(
project: !empty($data['project']) ? new Document($data['project']) : null,
user: !empty($data['user']) ? new Document($data['user']) : null,
type: $data['type'] ?? '',
table: !empty($data['table']) ? new Document($data['table']) : null,
row: !empty($data['row']) ? new Document($data['row']) : null,
collection: !empty($data['collection']) ? new Document($data['collection']) : null,
document: !empty($data['document']) ? new Document($data['document']) : null,
database: !empty($data['database']) ? new Document($data['database']) : null,
events: $data['events'] ?? [],
);
}
}
-45
View File
@@ -1,45 +0,0 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Delete extends Base
{
public function __construct(
public readonly ?Document $project = null,
public readonly string $type = '',
public readonly ?Document $document = null,
public readonly ?string $resource = null,
public readonly ?string $resourceType = null,
public readonly ?string $datetime = null,
public readonly ?string $hourlyUsageRetentionDatetime = null,
) {
}
public function toArray(): array
{
return [
'project' => $this->project?->getArrayCopy(),
'type' => $this->type,
'document' => $this->document?->getArrayCopy(),
'resource' => $this->resource,
'resourceType' => $this->resourceType,
'datetime' => $this->datetime,
'hourlyUsageRetentionDatetime' => $this->hourlyUsageRetentionDatetime,
];
}
public static function fromArray(array $data): static
{
return new self(
project: !empty($data['project']) ? new Document($data['project']) : null,
type: $data['type'] ?? '',
document: !empty($data['document']) ? new Document($data['document']) : null,
resource: $data['resource'] ?? null,
resourceType: $data['resourceType'] ?? null,
datetime: $data['datetime'] ?? null,
hourlyUsageRetentionDatetime: $data['hourlyUsageRetentionDatetime'] ?? null,
);
}
}
-92
View File
@@ -1,92 +0,0 @@
<?php
namespace Appwrite\Event\Message;
use Appwrite\Event\Event;
use Utopia\Config\Config;
use Utopia\Database\Document;
final class Func extends Base
{
public function __construct(
public readonly ?Document $project = null,
public readonly ?Document $user = null,
public readonly ?string $userId = null,
public readonly ?Document $function = null,
public readonly ?string $functionId = null,
public readonly ?Document $execution = null,
public readonly string $type = '',
public readonly string $jwt = '',
public readonly array $payload = [],
public readonly array $events = [],
public readonly string $body = '',
public readonly string $path = '',
public readonly array $headers = [],
public readonly string $method = '',
public readonly array $platform = [],
) {
}
public static function fromEvent(
string $event,
array $params,
?Document $project = null,
?Document $user = null,
?string $userId = null,
array $payload = [],
array $platform = [],
): static {
return new self(
project: $project,
user: $user,
userId: $userId,
payload: $payload,
events: $event !== '' ? Event::generateEvents($event, $params) : [],
platform: $platform,
);
}
public function toArray(): array
{
$platform = !empty($this->platform) ? $this->platform : Config::getParam('platform', []);
return [
'project' => $this->project?->getArrayCopy(),
'user' => $this->user?->getArrayCopy(),
'userId' => $this->userId,
'function' => $this->function?->getArrayCopy(),
'functionId' => $this->functionId,
'execution' => $this->execution?->getArrayCopy(),
'type' => $this->type,
'jwt' => $this->jwt,
'payload' => $this->payload,
'events' => $this->events,
'body' => $this->body,
'path' => $this->path,
'headers' => $this->headers,
'method' => $this->method,
'platform' => $platform,
];
}
public static function fromArray(array $data): static
{
return new self(
project: !empty($data['project']) ? new Document($data['project']) : null,
user: !empty($data['user']) ? new Document($data['user']) : null,
userId: $data['userId'] ?? null,
function: !empty($data['function']) ? new Document($data['function']) : null,
functionId: $data['functionId'] ?? null,
execution: !empty($data['execution']) ? new Document($data['execution']) : null,
type: $data['type'] ?? '',
jwt: $data['jwt'] ?? '',
payload: $data['payload'] ?? [],
events: $data['events'] ?? [],
body: $data['body'] ?? '',
path: $data['path'] ?? '',
headers: $data['headers'] ?? [],
method: $data['method'] ?? '',
platform: $data['platform'] ?? [],
);
}
}
-66
View File
@@ -1,66 +0,0 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Config\Config;
use Utopia\Database\Document;
final class Mail extends Base
{
public function __construct(
public readonly ?Document $project = null,
public readonly string $recipient = '',
public readonly string $name = '',
public readonly string $subject = '',
public readonly string $bodyTemplate = '',
public readonly string $body = '',
public readonly string $preview = '',
public readonly array $smtp = [],
public readonly array $variables = [],
public readonly array $attachment = [],
public readonly array $customMailOptions = [],
public readonly array $events = [],
public readonly array $platform = [],
) {
}
public function toArray(): array
{
$platform = !empty($this->platform) ? $this->platform : Config::getParam('platform', []);
return [
'project' => $this->project?->getArrayCopy(),
'recipient' => $this->recipient,
'name' => $this->name,
'subject' => $this->subject,
'bodyTemplate' => $this->bodyTemplate,
'body' => $this->body,
'preview' => $this->preview,
'smtp' => $this->smtp,
'variables' => $this->variables,
'attachment' => $this->attachment,
'customMailOptions' => $this->customMailOptions,
'events' => $this->events,
'platform' => $platform,
];
}
public static function fromArray(array $data): static
{
return new self(
project: !empty($data['project']) ? new Document($data['project']) : null,
recipient: $data['recipient'] ?? '',
name: $data['name'] ?? '',
subject: $data['subject'] ?? '',
bodyTemplate: $data['bodyTemplate'] ?? '',
body: $data['body'] ?? '',
preview: $data['preview'] ?? '',
smtp: $data['smtp'] ?? [],
variables: $data['variables'] ?? [],
attachment: $data['attachment'] ?? [],
customMailOptions: $data['customMailOptions'] ?? [],
events: $data['events'] ?? [],
platform: $data['platform'] ?? [],
);
}
}

Some files were not shown because too many files have changed in this diff Show More