Compare commits

..
Author SHA1 Message Date
Chirag AggarwalandGitHub 838dbc52e3 Merge branch '1.9.x' into t3code/sentry-bug-fix 2026-04-22 16:23:25 +05:30
Chirag Aggarwal e7d9ef74c4 Fix deployment single chunk content range
Fixes CLOUD-3JN6
2026-04-22 16:05:12 +05:30
Matej BačoandGitHub af531ee4f9 Merge pull request #11964 from appwrite/feat-public-project-policies
Feat: public project policies
2026-04-22 10:23:10 +02:00
Matej Bačo 72bb6378c2 Leftover 2026-04-22 10:00:19 +02:00
Matej Bačo bfa1960d8a Remove unneeded const 2026-04-22 10:00:10 +02:00
Matej Bačo e530bf41f7 Post-merge fix 2026-04-22 09:59:00 +02:00
Matej Bačo 0d27c59cb8 Merge branch '1.9.x' into feat-public-project-policies 2026-04-22 09:57:48 +02:00
Matej BačoandGitHub 97e611029e Merge pull request #11900 from appwrite/feat-project-smtp-endpoints
Feat: Public API for project SMTP endpoints
2026-04-22 09:52:09 +02:00
Matej Bačo efc37c68ec Merge branch '1.9.x' into feat-project-smtp-endpoints 2026-04-22 09:50:08 +02:00
Chirag AggarwalandGitHub 5b6dd5f75a Merge pull request #11949 from appwrite/chore/phpstan-level-4 2026-04-22 11:17:21 +05:30
Jake BarnbyandGitHub 2c973387c5 Merge pull request #11967 from appwrite/fix/listrows-total-int-cast
fix: cast cached total to int in listDocuments/listRows
2026-04-22 17:02:43 +12:00
Jake BarnbyandClaude Opus 4.7 8a841a6971 fix: cast cached total to int in listDocuments/listRows
Redis stringifies scalars on save, so on a cache hit the `total` field
was served as a string. Flutter SDK (and any strictly-typed client) then
failed with `TypeError: "37": type 'String' is not a subtype of type 'int'`.
The cache-miss path returned an int from `count()`, which is why only
repeat requests with `ttl > 0` tripped the bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:14:30 +12:00
Chirag Aggarwal c973ca0a5d Address PHPStan level 4 review feedback 2026-04-22 09:34:03 +05:30
Damodar LohaniandGitHub af05ad5784 Merge pull request #11953 from appwrite/CLO-4204-slow-query-hook
feat: add afterQuery hook to listRows (tablesdb) action
2026-04-22 07:25:24 +05:45
Matej Bačo 39af9c544d Remove unnessessary backwards compatibility 2026-04-21 18:22:44 +02:00
Luke B. SilverandGitHub 10b6955e98 Merge pull request #11965 from appwrite/fix/support-worker-num
fix: honor _APP_WORKERS_NUM in realtime
2026-04-21 17:14:08 +01:00
loks0nandClaude Opus 4.7 7e963e8439 fix: honor _APP_WORKERS_NUM in realtime
Realtime was ignoring _APP_WORKERS_NUM and always computing workers as
CPU × _APP_WORKER_PER_CORE, making it impossible to cap the worker count
without also changing the per-core multiplier. Prefer _APP_WORKERS_NUM
when set, falling back to the CPU × per-core calculation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:55:56 +01:00
Matej Bačo d9d5a81337 Fix 0->null backwrds compatibility 2026-04-21 17:51:31 +02:00
Matej Bačo 193891a7c9 Fix analyzer 2026-04-21 17:38:32 +02:00
Matej Bačo 70cb5ca68a linter fix 2026-04-21 17:34:59 +02:00
Matej Bačo c8a1746119 Fix existing tests 2026-04-21 17:34:44 +02:00
Matej Bačo 06eb550e98 Finalize tests 2026-04-21 16:56:00 +02:00
Matej Bačo 6c89a05a60 Fix 0 session to mean unlimited 2026-04-21 16:38:53 +02:00
Matej BačoandGitHub 1b993ede0f Merge branch '1.9.x' into feat-project-smtp-endpoints 2026-04-21 16:15:00 +02:00
Matej Bačo 648ffcdcfa Fix event labels 2026-04-21 16:07:52 +02:00
Matej Bačo 0be94a7aff Fix tests 2026-04-21 15:57:30 +02:00
Matej Bačo eba145ee2d More integration tests 2026-04-21 15:13:27 +02:00
Matej Bačo 99d230c70d Integration tests 2026-04-21 14:39:02 +02:00
Chirag Aggarwal 4ac1b68bbc Fix OpenAPI enum keys analysis 2026-04-21 18:03:18 +05:30
Matej Bačo 6adabae620 Add policy tests 2026-04-21 14:05:46 +02:00
Matej Bačo 5f9dc0fcd8 Req & res filters, review fixes 2026-04-21 13:58:36 +02:00
Chirag AggarwalandGitHub da4dcd8505 Merge branch '1.9.x' into chore/phpstan-level-4 2026-04-21 17:08:46 +05:30
Matej Bačo d0f853d4cd Add more project policies 2026-04-21 13:38:27 +02:00
Matej Bačo 2f272f0480 Cleanup unused markdowns descriptions 2026-04-21 13:27:08 +02:00
Matej Bačo 4317ee5617 Move some of auth settings to project policies 2026-04-21 13:11:42 +02:00
Damodar LohaniandGitHub 1dc9217c65 Merge branch '1.9.x' into CLO-4204-slow-query-hook 2026-04-21 14:31:45 +05:45
Damodar Lohani 50bd2877f4 chore: shorten afterQuery docblock 2026-04-21 08:46:21 +00:00
Damodar Lohani f465e2267a chore: tighten afterQuery docblock 2026-04-21 08:43:20 +00:00
Damodar Lohani b4f1652286 refactor: simplify afterQuery DB timing to single wall-clock bracket
Replaces the per-call $measure closure with a single $dbStart
timestamp taken right before the fetch block and a single subtraction
right after it. Drops 6 lines of HOF indirection plus the $measure
variable, at the cost of including cache GET/SET time (~0.5–5ms) in
measurements when ttl > 0. For slow-query logging at a 100ms+
threshold that noise is negligible, and the default ttl=0 path has
no cache ops at all so the measurement is pure DB engine time.

The bracket captures the cursor lookup, find/count, and transaction
state calls — everything between "query parsed" and "fetch done",
as intended. processDocument's post-fetch relationship work is still
outside the bracket, matching the original design.
2026-04-21 08:41:11 +00:00
Damodar Lohani 4e4860e7f8 refactor: move afterQuery hook into base listDocuments action
Moves the DB-duration measurement and afterQuery() hook from the
tablesDB-specific Rows/XList into the shared
Databases/Collections/Documents/XList base. Because TablesDB Rows and
DocumentsDB Documents both extend the legacy listDocuments base, a
single override now covers all three endpoints: legacy
listDocuments, listDocumentsDBDocuments, and tablesDB listRows.

TablesDB Rows drops the ~200-line action() duplicate and keeps only
the path/params/SDK overrides it needs, plus the extra
->inject('utopia') so its injection chain matches the new base action
signature. DocumentsDB Documents gets the same one-line inject
addition. Net -165 lines of duplication removed.

Behaviour is unchanged for CE (afterQuery() is a no-op); downstream
distributions overriding afterQuery() now observe every list-documents
/ list-rows call site for free.
2026-04-21 08:28:12 +00:00
Matej Bačo cce04b3bd1 Improve test coverage 2026-04-21 10:28:11 +02:00
Damodar LohaniandGitHub 82af29c0cf Merge branch '1.9.x' into CLO-4204-slow-query-hook 2026-04-21 14:04:29 +05:45
Damodar Lohani 5724d19426 Merge remote-tracking branch 'origin/CLO-4204-slow-query-hook' into CLO-4204-slow-query-hook 2026-04-21 04:34:38 +00:00
Damodar Lohani 7c84337c5f Merge remote-tracking branch 'origin/1.9.x' into CLO-4204-slow-query-hook 2026-04-21 04:32:28 +00:00
Matej Bačo b1d37bc4be Fix remaining test failures 2026-04-20 23:03:16 +02:00
Matej Bačo 4808cad081 Fix most of Project tests 2026-04-20 23:01:53 +02:00
Matej Bačo 72cd7671c4 Fix projects tests 2026-04-20 23:01:00 +02:00
Matej Bačo c3e411fcaa Fix tests 2026-04-20 22:47:47 +02:00
Matej Bačo 8c31e9f206 Server error fixes 2026-04-20 22:36:47 +02:00
Matej Bačo a4ad1b6df3 Code quality improvs 2026-04-20 22:35:35 +02:00
Matej Bačo 9e94f15f02 Finalize tests 2026-04-20 22:23:34 +02:00
Matej Bačo e27719108b Add tests 2026-04-20 22:15:03 +02:00
Matej Bačo 8bf2f54a51 Leftovers 2026-04-20 22:04:46 +02:00
Matej Bačo 8c03db70e9 Finalize templates design 2026-04-20 22:04:20 +02:00
Matej Bačo 8ea69e0321 Fix password visibility test 2026-04-20 16:57:23 +02:00
Matej Bačo dfec2b3cb7 Improve test coverage 2026-04-20 16:11:22 +02:00
Matej Bačo 848f09956e Improve backwards compatibility test coverage 2026-04-20 15:36:49 +02:00
Matej Bačo 51f50b161c Improved backwards compatibility 2026-04-20 15:28:05 +02:00
Matej Bačo 52e3319a86 Linter fix 2026-04-20 14:50:12 +02:00
Matej Bačo 8b41aed919 Post-merge removal 2026-04-20 14:49:43 +02:00
Matej Bačo ba4430801d Merge branch 'feat-project-templates-api' into feat-project-smtp-endpoints 2026-04-20 14:49:04 +02:00
Matej Bačo 1c1ec43150 Removeal post-merge 2026-04-20 14:47:01 +02:00
Matej Bačo 2f62cced0a Merge branch '1.9.x' into feat-project-smtp-endpoints 2026-04-20 14:46:42 +02:00
Matej Bačo afb8f70316 Fix tests 2026-04-20 14:42:52 +02:00
Matej Bačo 2097b0a0b0 Better support for post-smtp changes 2026-04-20 14:04:53 +02:00
Matej Bačo 78ef52cc9e Manual QA fixes 2026-04-20 13:11:11 +02:00
Matej Bačo f040a4dc31 More backwards compatibility 2026-04-20 11:58:55 +02:00
Matej Bačo 56385ce167 Backwards compatibility 2026-04-20 11:48:45 +02:00
Matej Bačo bc592903db Support reply to name 2026-04-20 11:47:06 +02:00
Matej Bačo 06a2d48deb Linter fix 2026-04-20 11:29:13 +02:00
Matej Bačo c4d9c3dc4f Improve endpoint quality 2026-04-20 11:28:21 +02:00
Damodar LohaniandGitHub b36f1edeb0 Merge branch '1.9.x' into CLO-4204-slow-query-hook 2026-04-20 14:04:10 +05:45
Damodar Lohani eef443f07e chore: bump utopia-php/database to 5.3.22 for Query::fingerprint 2026-04-20 07:53:36 +00:00
Damodar Lohani a5c0a920ba feat: add afterQuery hook to list-documents/rows action
Wrap each database call (find, count, transaction list/count) with a
measuring closure so the actual DB duration is known — cache hits
report near-zero, cache misses report only the DB time, not cache
save / response serialization.

After the response is sent, invoke a protected afterQuery() hook with
the measured duration, the database/collection documents, and both
parsed + raw query arrays. CE impl is a no-op; downstreams (e.g.,
cloud) can override it to log slow queries without relying on HTTP
shutdown hooks or route-path matching.

Exceptions from afterQuery are swallowed so observability never
breaks the response.
2026-04-20 05:32:49 +00:00
Chirag Aggarwal 37a2b1cbd9 fix: restore executions limit cleanup behind a runtime env flag
Per review feedback on the PHPStan cleanup, the two `if
($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE)`
blocks in `app/controllers/general.php` and
`src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php`
were load-bearing feature flags, not dead code. Removing them silently
dropped the ability to turn the cleanup on later.

Changes:

- Convert `ENABLE_EXECUTIONS_LIMIT_ON_ROUTE` from
  `const ... = false;` to a `define()` backed by the new
  `_APP_EXECUTIONS_LIMIT_ON_ROUTE` env var (defaults to `disabled`).
  PHPStan can no longer fold the `&&` away since the value is now
  runtime-resolved, so the guarded blocks are live again.
- Restore the `/* cleanup */` block in the `router()` helper in
  `app/controllers/general.php`.
- Restore the two cleanup blocks in `Functions/Http/Executions/Create.php`
  (one on the async-scheduled return path, one on the sync-response
  path), and re-add the `DeleteEvent $queueForDeletes` /
  `int $executionsRetentionCount` injections plus the
  `Appwrite\Event\Delete` import.

Runtime behavior is identical to main (flag off by default); operators
can now flip it via env without a code change.
2026-04-20 08:54:31 +05:30
Matej Bačo 6b66923f18 Fix delete response placeholder audit label 2026-04-19 19:36:24 +02:00
Chirag Aggarwal adb4e4ef36 ci: fix benchmark by pulling compose from GitHub raw for the latest tag
`https://appwrite.io/install/compose` now returns a 308 redirect to the
HTML install docs (`/docs/advanced/self-hosting/installation`) instead
of serving the compose file, so the Benchmark job's "Installing latest
version" step was downloading 0 bytes and `docker compose up -d` died
with "empty compose file". This has been failing the Benchmark job on
every recent PR, not just this one.

Resolve the latest release tag via the GitHub API, then fetch the
compose file and `.env` from `raw.githubusercontent.com` at that tag.
Switched both curl calls to `-fsSL` so they fail loudly on non-2xx
responses or redirect loss instead of silently writing empty files.
2026-04-19 20:34:51 +05:30
Chirag Aggarwal d86258a6f6 fix: restore runtime guards and widen types missed by PHPStan cleanup
Three follow-ups from CI that the level-4 pass got wrong:

1. `account.php` / `users.php`: `Document::find()` returns `mixed`
   (specifically `Document|false` in practice), not `Document`. The
   earlier `@var Document $oldTarget` docblocks were lies, and the
   runtime `instanceof Document` guards were load-bearing — removing
   them caused `Call to a member function isEmpty() on false` 500s
   on the `PATCH /v1/users/:id/email` and `/phone` endpoints (and the
   analogous `/v1/account/email`, `/v1/account/phone` flows). Dropped
   the misleading `@var` docblocks and restored
   `$oldTarget instanceof Document && !$oldTarget->isEmpty()`.

2. `Installer/Runtime/Config::setEnabledDatabases()` is a boundary
   that actually takes arbitrary user/compose input — not a trusted
   `string[]`. The `is_string($v)` filter was covering for that, and
   `ConfigTest::testSetEnabledDatabasesFiltersInvalid` explicitly
   asserts it. Widened the PHPDoc to `array<mixed>` and restored
   `is_string($v) && $v !== ''` in the filter.

3. `OAuth2/Apple::getAppSecret()` wrapped `json_decode` in a
   `try/catch (\Throwable)` — but `json_decode` without
   `JSON_THROW_ON_ERROR` returns `null` on failure, it doesn't throw.
   PHP 8.3's PHPStan flagged the catch as dead (PHP 8.5 didn't, which
   is why it slipped through locally). Replaced with
   `if (!\is_array($secret)) throw`, which preserves the original
   "invalid secret" guard.
2026-04-19 17:52:51 +05:30
Chirag Aggarwal d2230f8fe7 chore: bump PHPStan to level 4 and fix all new errors
Raises `phpstan.neon` level from 3 to 4 and fixes the 549 new errors
that level 4 surfaces across 157 files. Fixes are root-cause — no
`@phpstan-ignore`, no `@var` casts, no baseline entries, no widened
types. A handful of latent bugs were fixed along the way:

- `app/controllers/general.php`: path-traversal guard was negating
  `\substr(...)` before the strict comparison (`!\substr(...) === $base`
  was always `false === $base`). Rewritten as `\substr(...) !== $base`.
- `src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php`
  and `.../TablesDB/Logs/XList.php`: were importing the raw Matomo
  `DeviceDetector` (whose `getDevice()` returns `?int`) but treating the
  result as an array with `deviceName/deviceBrand/deviceModel` keys.
  Swapped to `Appwrite\Detector\Detector`, matching the wrapper already
  used a few lines below for `$os`/`$client`.
- `src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`: a match
  key was checking `$resourceKey === 'functions'` when `$resourceKey`
  is `'functionId'|'siteId'` — always false. Switched to the intended
  `$resource->getCollection() === 'functions'` check.
- `src/Appwrite/OpenSSL/OpenSSL.php`: `encrypt()` return type tightened
  to `string|false` to match `openssl_encrypt`; this lets callers'
  `=== false` error handling remain meaningful.
- `app/controllers/api/messaging.php`: removed a dead
  `array_key_exists('from', [])` branch in the Msg91 provider (empty
  array literal; branch was unreachable).

Large cleanup categories across the 549 fixes:
- Removed redundant `?? default` on array offsets and expressions that
  PHPStan now knows are non-nullable.
- Removed unreachable statements (mostly `return;` after `throw` or
  `markTestSkipped()`).
- Removed redundant `is_array`/`is_string`/`is_bool`/`instanceof` checks
  on already-narrowed types.
- Added `default =>` arms (or throwing arms) to non-exhaustive matches
  on `string`/`mixed` input.
- Removed dead `$document === false` branches where method return types
  were tightened to non-nullable `Document`.
- Removed unused properties (`$version` on Etsy/Zoom OAuth2, `$paths` on
  Installer State, `$source` on MigrationsWorker, `$account2` on two
  GraphQL auth tests), unused traits (`ApiVectorsDB`, `DatabaseFixture`),
  and an unused `cleanupStaleExecutions` task method.
- Replaced `assertTrue(true)` and redundant `assertIsArray`/`assertIsString`/
  `assertNotNull` assertions with `addToAssertionCount(1)` or
  `assertNotEmpty` where the runtime type was already known.
2026-04-19 17:31:20 +05:30
Matej BačoandClaude Opus 4.7 69d53cb2d4 Remove unused email template list response model
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:03:36 +02:00
Matej Bačo 447375dcbf Fix tests 2026-04-19 10:53:11 +02:00
Matej Bačo afab349a77 self review fixes 2026-04-19 10:43:57 +02:00
Matej Bačo 2a95cfd5a3 Final template API rework 2026-04-19 10:35:57 +02:00
Matej BačoandClaude Opus 4.7 8a1f8c71b2 Temporary removal of listEmailTemplates
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:15:48 +02:00
Matej Bačo 0a71efc244 improve test coverage 2026-04-18 11:23:53 +02:00
Matej Bačo fcc7a56f4a Fix list endpoint 2026-04-18 11:01:26 +02:00
Matej Bačo be5eeb1aba Fix failing tests 2026-04-17 19:50:34 +02:00
Matej Bačo e00efaea68 Fix bug 2026-04-17 18:15:49 +02:00
Matej Bačo 64d182ac6a Add tests for templates 2026-04-17 17:49:20 +02:00
Matej Bačo bb38bf4248 Improve code quality 2026-04-17 17:42:46 +02:00
Matej Bačo dc704fdb51 Improved xlist endpoint 2026-04-17 17:27:19 +02:00
Matej Bačo e388d2f6a3 List tempaltes endpoint 2026-04-17 17:22:55 +02:00
Matej Bačo b01ec03723 Fix analyze bug 2026-04-17 17:01:27 +02:00
Matej Bačo 489b2c4e21 Add new scopes 2026-04-17 16:45:04 +02:00
Matej Bačo 1a46fc2006 Move template APIs under project API 2026-04-17 16:43:17 +02:00
Matej Bačo 7fe65eec57 Restruture endpoints 2026-04-16 10:23:31 +02:00
Matej Bačo 732538504d Fix backwards compatibility 2026-04-14 17:42:26 +02:00
Matej Bačo d3dcbfe567 Leftover chaneges, 2026-04-14 17:20:22 +02:00
Matej Bačo 905d2a8eaa Fix tests 2026-04-14 17:20:03 +02:00
Matej Bačo b5e46c1a60 E2E integration tests 2026-04-14 17:09:37 +02:00
Matej Bačo a3a8ad88fe formatting fix 2026-04-14 17:07:06 +02:00
Matej Bačo 9d6428d5d5 Improve tests 2026-04-14 17:06:57 +02:00
Matej Bačo d7f8ca3f01 Improve endpoint quality 2026-04-14 17:04:22 +02:00
Matej Bačo 56bcc0d09f Fix tests 2026-04-14 17:01:56 +02:00
Matej Bačo 8b0c60d2f9 Fix bugs 2026-04-14 17:01:46 +02:00
Matej Bačo ffdfe4bbf8 Add new tests 2026-04-14 16:55:39 +02:00
Matej Bačo 556ca10ed9 Register endpoints 2026-04-14 16:52:38 +02:00
Matej Bačo 193beb76fe add SMTP endpoints 2026-04-14 16:50:07 +02:00
307 changed files with 6623 additions and 2094 deletions
+4 -3
View File
@@ -692,10 +692,11 @@ jobs:
- name: Installing latest version
run: |
rm docker-compose.yml
rm .env
curl https://appwrite.io/install/compose -o docker-compose.yml
curl https://appwrite.io/install/env -o .env
LATEST_TAG=$(curl -fsSL -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/appwrite/appwrite/releases/latest | jq -r .tag_name)
echo "Latest release tag: $LATEST_TAG"
curl -fsSL "https://raw.githubusercontent.com/appwrite/appwrite/${LATEST_TAG}/docker-compose.yml" -o docker-compose.yml
curl -fsSL "https://raw.githubusercontent.com/appwrite/appwrite/${LATEST_TAG}/.env" -o .env
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
docker compose up -d
sleep 10
+3 -3
View File
@@ -72,7 +72,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
### Windows
@@ -84,7 +84,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
#### PowerShell
@@ -94,7 +94,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+3 -3
View File
@@ -75,7 +75,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
### Windows
@@ -88,7 +88,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
#### PowerShell
@@ -99,7 +99,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.1
appwrite/appwrite:1.9.0
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
+3
View File
@@ -55,6 +55,9 @@ $admins = [
'tables.write',
'platforms.read',
'platforms.write',
'policies.write',
'templates.read',
'templates.write',
'projects.write',
'keys.read',
'keys.write',
+12
View File
@@ -204,4 +204,16 @@ return [ // List of publicly visible scopes
"description" =>
"Access to create, update, and delete project\'s platforms",
],
"policies.write" => [
"description" =>
"Access to update project\'s policies",
],
"templates.read" => [
"description" =>
"Access to read project\'s templates",
],
"templates.write" => [
"description" =>
"Access to create, update, and delete project\'s templates",
],
];
+99 -61
View File
@@ -133,9 +133,6 @@ $createSession = function (string $userId, string $secret, Request $request, Res
});
$provider = match ($verifiedToken->getAttribute('type')) {
TOKEN_TYPE_VERIFICATION,
TOKEN_TYPE_RECOVERY,
TOKEN_TYPE_INVITE => SESSION_PROVIDER_EMAIL,
TOKEN_TYPE_MAGIC_URL => SESSION_PROVIDER_MAGIC_URL,
TOKEN_TYPE_PHONE => SESSION_PROVIDER_PHONE,
TOKEN_TYPE_OAUTH2 => $oauthProvider,
@@ -335,15 +332,15 @@ Http::post('/v1/account')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -453,7 +450,7 @@ Http::delete('/v1/account')
->groups(['api', 'account'])
->label('scope', 'account')
->label('audits.event', 'user.delete')
->label('audits.resource', 'user/{response.$id}')
->label('audits.resource', 'user/{user.$id}')
->label('sdk', new Method(
namespace: 'account',
group: 'account',
@@ -837,7 +834,7 @@ Http::patch('/v1/account/sessions/:sessionId')
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
if (!empty($provider) && $className !== null && \class_exists($className)) {
if (!empty($provider) && \class_exists($className)) {
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
@@ -1604,7 +1601,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
}
}
if ($user === false || $user->isEmpty()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
if ($user->isEmpty()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
if (empty($email)) {
$failureRedirect(Exception::USER_UNAUTHORIZED, 'OAuth provider failed to return email.');
}
@@ -1621,7 +1618,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
}
// If user is not found, check if there is a user with the same email
if ($user === false || $user->isEmpty()) {
if ($user->isEmpty()) {
$userWithEmail = $dbForProject->findOne('users', [
Query::equal('email', [$email]),
]);
@@ -1634,7 +1631,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
}
// If user is not found, check if there is an identity with the same email
if ($user === false || $user->isEmpty()) {
if ($user->isEmpty()) {
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
@@ -1646,7 +1643,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
}
}
if ($user === false || $user->isEmpty()) { // Last option -> create the user
if ($user->isEmpty()) { // Last option -> create the user
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if ($limit !== 0) {
@@ -1679,15 +1676,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
@@ -1820,15 +1817,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
@@ -1954,7 +1951,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
}
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
if (isset($sessionUpgrade) && isset($session)) {
foreach ($user->getAttribute('targets', []) as $target) {
if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
continue;
@@ -2178,15 +2175,15 @@ Http::post('/v1/account/tokens/magic-url')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2305,8 +2302,8 @@ Http::post('/v1/account/tokens/magic-url')
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyTo = "";
$replyToEmail = '';
$replyToName = '';
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2315,8 +2312,13 @@ Http::post('/v1/account/tokens/magic-url')
if (!empty($smtp['senderName'])) {
$senderName = $smtp['senderName'];
}
if (!empty($smtp['replyTo'])) {
$replyTo = $smtp['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
if (!empty($smtpReplyToEmail)) {
$replyToEmail = $smtpReplyToEmail;
}
if (!empty($smtp['replyToName'])) {
$replyToName = $smtp['replyToName'];
}
$queueForMails
@@ -2333,8 +2335,13 @@ Http::post('/v1/account/tokens/magic-url')
if (!empty($customTemplate['senderName'])) {
$senderName = $customTemplate['senderName'];
}
if (!empty($customTemplate['replyTo'])) {
$replyTo = $customTemplate['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
if (!empty($customReplyToEmail)) {
$replyToEmail = $customReplyToEmail;
}
if (!empty($customTemplate['replyToName'])) {
$replyToName = $customTemplate['replyToName'];
}
$body = $customTemplate['message'] ?? '';
@@ -2342,7 +2349,8 @@ Http::post('/v1/account/tokens/magic-url')
}
$queueForMails
->setSmtpReplyTo($replyTo)
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
@@ -2488,15 +2496,15 @@ Http::post('/v1/account/tokens/email')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2623,7 +2631,8 @@ Http::post('/v1/account/tokens/email')
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyTo = "";
$replyToEmail = '';
$replyToName = '';
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2632,8 +2641,13 @@ Http::post('/v1/account/tokens/email')
if (!empty($smtp['senderName'])) {
$senderName = $smtp['senderName'];
}
if (!empty($smtp['replyTo'])) {
$replyTo = $smtp['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
if (!empty($smtpReplyToEmail)) {
$replyToEmail = $smtpReplyToEmail;
}
if (!empty($smtp['replyToName'])) {
$replyToName = $smtp['replyToName'];
}
$queueForMails
@@ -2650,8 +2664,13 @@ Http::post('/v1/account/tokens/email')
if (!empty($customTemplate['senderName'])) {
$senderName = $customTemplate['senderName'];
}
if (!empty($customTemplate['replyTo'])) {
$replyTo = $customTemplate['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
if (!empty($customReplyToEmail)) {
$replyToEmail = $customReplyToEmail;
}
if (!empty($customTemplate['replyToName'])) {
$replyToName = $customTemplate['replyToName'];
}
$body = $customTemplate['message'] ?? '';
@@ -2659,7 +2678,8 @@ Http::post('/v1/account/tokens/email')
}
$queueForMails
->setSmtpReplyTo($replyTo)
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
@@ -3274,7 +3294,7 @@ Http::patch('/v1/account/password')
}
$history[] = $newPassword;
$history = array_slice($history, (count($history) - $historyLimit), $historyLimit);
$history = array_slice($history, -$historyLimit);
}
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
@@ -3397,15 +3417,15 @@ Http::patch('/v1/account/email')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -3437,9 +3457,6 @@ Http::patch('/v1/account/email')
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
@@ -3526,9 +3543,6 @@ Http::patch('/v1/account/phone')
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
@@ -3752,7 +3766,8 @@ Http::post('/v1/account/recovery')
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyTo = "";
$replyToEmail = '';
$replyToName = '';
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -3761,8 +3776,13 @@ Http::post('/v1/account/recovery')
if (!empty($smtp['senderName'])) {
$senderName = $smtp['senderName'];
}
if (!empty($smtp['replyTo'])) {
$replyTo = $smtp['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
if (!empty($smtpReplyToEmail)) {
$replyToEmail = $smtpReplyToEmail;
}
if (!empty($smtp['replyToName'])) {
$replyToName = $smtp['replyToName'];
}
$queueForMails
@@ -3779,8 +3799,13 @@ Http::post('/v1/account/recovery')
if (!empty($customTemplate['senderName'])) {
$senderName = $customTemplate['senderName'];
}
if (!empty($customTemplate['replyTo'])) {
$replyTo = $customTemplate['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
if (!empty($customReplyToEmail)) {
$replyToEmail = $customReplyToEmail;
}
if (!empty($customTemplate['replyToName'])) {
$replyToName = $customTemplate['replyToName'];
}
$body = $customTemplate['message'] ?? '';
@@ -3788,7 +3813,8 @@ Http::post('/v1/account/recovery')
}
$queueForMails
->setSmtpReplyTo($replyTo)
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
@@ -4071,7 +4097,8 @@ Http::post('/v1/account/verifications/email')
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyTo = "";
$replyToEmail = '';
$replyToName = '';
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -4080,8 +4107,13 @@ Http::post('/v1/account/verifications/email')
if (!empty($smtp['senderName'])) {
$senderName = $smtp['senderName'];
}
if (!empty($smtp['replyTo'])) {
$replyTo = $smtp['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
if (!empty($smtpReplyToEmail)) {
$replyToEmail = $smtpReplyToEmail;
}
if (!empty($smtp['replyToName'])) {
$replyToName = $smtp['replyToName'];
}
$queueForMails
@@ -4098,8 +4130,13 @@ Http::post('/v1/account/verifications/email')
if (!empty($customTemplate['senderName'])) {
$senderName = $customTemplate['senderName'];
}
if (!empty($customTemplate['replyTo'])) {
$replyTo = $customTemplate['replyTo'];
// Includes backwards compatibility: fall back to legacy `replyTo` key
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
if (!empty($customReplyToEmail)) {
$replyToEmail = $customReplyToEmail;
}
if (!empty($customTemplate['replyToName'])) {
$replyToName = $customTemplate['replyToName'];
}
$body = $customTemplate['message'] ?? '';
@@ -4107,7 +4144,8 @@ Http::post('/v1/account/verifications/email')
}
$queueForMails
->setSmtpReplyTo($replyTo)
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
@@ -4618,7 +4656,7 @@ Http::delete('/v1/account/targets/:targetId/push')
->groups(['api', 'account'])
->label('scope', 'targets.write')
->label('audits.event', 'target.delete')
->label('audits.resource', 'target/response.$id')
->label('audits.resource', 'target/{request.targetId}')
->label('event', 'users.[userId].targets.[targetId].delete')
->label('sdk', new Method(
namespace: 'account',
+1 -14
View File
@@ -482,7 +482,6 @@ Http::post('/v1/messaging/providers/msg91')
$enabled === true
&& \array_key_exists('senderId', $credentials)
&& \array_key_exists('authKey', $credentials)
&& \array_key_exists('from', $options)
) {
$enabled = true;
} else {
@@ -3207,10 +3206,6 @@ Http::post('/v1/messaging/messages/email')
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
}
$mergedTargets = \array_merge($targets, $cc, $bcc);
if (!empty($mergedTargets)) {
@@ -3386,10 +3381,6 @@ Http::post('/v1/messaging/messages/sms')
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
}
if (!empty($targets)) {
$foundTargets = $dbForProject->find('targets', [
Query::equal('$id', $targets),
@@ -3527,10 +3518,6 @@ Http::post('/v1/messaging/messages/push')
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
}
if (!empty($targets)) {
$foundTargets = $dbForProject->find('targets', [
Query::equal('$id', $targets),
@@ -4660,7 +4647,7 @@ Http::delete('/v1/messaging/messages/:messageId')
if (!empty($scheduleId)) {
try {
$dbForPlatform->deleteDocument('schedules', $scheduleId);
} catch (Exception) {
} catch (\Throwable) {
// Ignore
}
}
+2 -1
View File
@@ -51,7 +51,8 @@ function getDatabaseTransferResourceServices(string $databaseType)
DATABASE_TYPE_LEGACY,
DATABASE_TYPE_TABLESDB => Transfer::GROUP_DATABASES_TABLES_DB,
DATABASE_TYPE_VECTORSDB => Transfer::GROUP_DATABASES_VECTOR_DB,
DATABASE_TYPE_DOCUMENTSDB => Transfer::GROUP_DATABASES_DOCUMENTS_DB
DATABASE_TYPE_DOCUMENTSDB => Transfer::GROUP_DATABASES_DOCUMENTS_DB,
default => throw new \LogicException('Unknown database type: ' . $databaseType),
};
}
+2 -1
View File
@@ -113,11 +113,12 @@ Http::get('/v1/project/usage')
$factor = match ($period) {
'1h' => 3600,
'1d' => 86400,
default => throw new \LogicException('Unsupported period: ' . $period),
};
$limit = match ($period) {
'1h' => (new DateTime($startDate))->diff(new DateTime($endDate))->days * 24,
'1d' => (new DateTime($startDate))->diff(new DateTime($endDate))->days
'1d' => (new DateTime($startDate))->diff(new DateTime($endDate))->days,
};
$format = match ($period) {
+4 -763
View File
@@ -3,29 +3,21 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Event\Delete;
use Appwrite\Event\Mail;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\Queries\Keys;
use Appwrite\Utopia\Response;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Validator\Email;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
@@ -138,162 +130,6 @@ Http::patch('/v1/projects/:projectId/oauth2')
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/auth/session-alerts')
->desc('Update project sessions emails')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'updateSessionAlerts',
description: '/docs/references/projects/update-session-alerts.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'])
->param('alerts', false, new Boolean(true), 'Set to true to enable session emails.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $alerts, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['sessionAlerts'] = $alerts;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/auth/memberships-privacy')
->desc('Update project memberships privacy attributes')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'updateMembershipsPrivacy',
description: '/docs/references/projects/update-memberships-privacy.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'])
->param('userName', true, new Boolean(true), 'Set to true to show userName to members of a team.')
->param('userEmail', true, new Boolean(true), 'Set to true to show email to members of a team.')
->param('mfa', true, new Boolean(true), 'Set to true to show mfa to members of a team.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $userName, bool $userEmail, bool $mfa, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['membershipsUserName'] = $userName;
$auths['membershipsUserEmail'] = $userEmail;
$auths['membershipsMfa'] = $mfa;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/auth/limit')
->desc('Update project users limit')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'updateAuthLimit',
description: '/docs/references/projects/update-auth-limit.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'])
->param('limit', false, new Range(0, APP_LIMIT_USERS), 'Set the max number of users allowed in this project. Use 0 for unlimited.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, int $limit, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['limit'] = $limit;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/auth/duration')
->desc('Update project authentication duration')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'updateAuthDuration',
description: '/docs/references/projects/update-auth-duration.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'])
->param('duration', 31536000, new Range(0, 31536000), 'Project session length in seconds. Max length: 31536000 seconds.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, int $duration, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['duration'] = $duration;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/auth/:method')
->desc('Update project auth method status. Use this endpoint to enable or disable a given auth method for this project.')
->groups(['api', 'projects'])
@@ -321,7 +157,6 @@ Http::patch('/v1/projects/:projectId/auth/:method')
$project = $dbForPlatform->getDocument('projects', $projectId);
$auth = Config::getParam('auth')[$method] ?? [];
$authKey = $auth['key'] ?? '';
$status = ($status === '1' || $status === 'true' || $status === 1 || $status === true);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
@@ -335,158 +170,6 @@ Http::patch('/v1/projects/:projectId/auth/:method')
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/auth/password-history')
->desc('Update authentication password history. Use this endpoint to set the number of password history to save and 0 to disable password history.')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'updateAuthPasswordHistory',
description: '/docs/references/projects/update-auth-password-history.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'])
->param('limit', 0, new Range(0, APP_LIMIT_USER_PASSWORD_HISTORY), 'Set the max number of passwords to store in user history. User can\'t choose a new password that is already stored in the password history list. Max number of passwords allowed in history is' . APP_LIMIT_USER_PASSWORD_HISTORY . '. Default value is 0')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, int $limit, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['passwordHistory'] = $limit;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/auth/password-dictionary')
->desc('Update authentication password dictionary status. Use this endpoint to enable or disable the dicitonary check for user password')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'updateAuthPasswordDictionary',
description: '/docs/references/projects/update-auth-password-dictionary.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'])
->param('enabled', false, new Boolean(false), 'Set whether or not to enable checking user\'s password against most commonly used passwords. Default is false.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['passwordDictionary'] = $enabled;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/auth/personal-data')
->desc('Update personal data check')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'updatePersonalDataCheck',
description: '/docs/references/projects/update-personal-data-check.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'])
->param('enabled', false, new Boolean(false), 'Set whether or not to check a password for similarity with personal data. Default is false.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['personalDataCheck'] = $enabled;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/auth/max-sessions')
->desc('Update project user sessions limit')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'updateAuthSessionsLimit',
description: '/docs/references/projects/update-auth-sessions-limit.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'])
->param('limit', false, new Range(1, APP_LIMIT_USER_SESSIONS_MAX), 'Set the max number of users allowed in this project. Value allowed is between 1-' . APP_LIMIT_USER_SESSIONS_MAX . '. Default is ' . APP_LIMIT_USER_SESSIONS_DEFAULT)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, int $limit, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['maxSessions'] = $limit;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/auth/mock-numbers')
->desc('Update the mock numbers for the project')
->groups(['api', 'projects'])
@@ -619,471 +302,29 @@ Http::post('/v1/projects/:projectId/jwts')
])]), Response::MODEL_JWT);
});
// CUSTOM SMTP and Templates
Http::patch('/v1/projects/:projectId/smtp')
->desc('Update SMTP')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'templates',
name: 'updateSmtp',
description: '/docs/references/projects/update-smtp.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.updateSMTP',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'templates',
name: 'updateSMTP',
description: '/docs/references/projects/update-smtp.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'])
->param('enabled', false, new Boolean(), 'Enable custom SMTP service')
->param('senderName', '', new Text(255, 0), 'Name of the email sender', true)
->param('senderEmail', '', new Email(), 'Email of the sender', true)
->param('replyTo', '', new Email(), 'Reply to email', true)
->param('host', '', new HostName(), 'SMTP server host name', true)
->param('port', 587, new Integer(), 'SMTP server port', true)
->param('username', '', new Text(0, 0), 'SMTP server username', true)
->param('password', '', new Text(0, 0), 'SMTP server password', true)
->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $enabled, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
// Ensure required params for when enabling SMTP
if ($enabled) {
if (empty($senderName)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Sender name is required when enabling SMTP.');
} elseif (empty($senderEmail)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Sender email is required when enabling SMTP.');
} elseif (empty($host)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Host is required when enabling SMTP.');
} elseif (empty($port)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Port is required when enabling SMTP.');
}
}
// validate SMTP settings
if ($enabled) {
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->SMTPAuth = (!empty($username) && !empty($password));
$mail->Username = $username;
$mail->Password = $password;
$mail->Host = $host;
$mail->Port = $port;
$mail->SMTPSecure = $secure;
$mail->SMTPAutoTLS = false;
$mail->Timeout = 5;
try {
$valid = $mail->SmtpConnect();
if (!$valid) {
throw new Exception('Connection is not valid.');
}
} catch (Throwable $error) {
throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, $error->getMessage());
}
}
// Save SMTP settings
if ($enabled) {
$smtp = [
'enabled' => $enabled,
'senderName' => $senderName,
'senderEmail' => $senderEmail,
'replyTo' => $replyTo,
'host' => $host,
'port' => $port,
'username' => $username,
'password' => $password,
'secure' => $secure,
];
} else {
$smtp = [
'enabled' => false
];
}
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('smtp', $smtp));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::post('/v1/projects/:projectId/smtp/tests')
->desc('Create SMTP test')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'templates',
name: 'createSmtpTest',
description: '/docs/references/projects/create-smtp-test.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.createSMTPTest',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'templates',
name: 'createSMTPTest',
description: '/docs/references/projects/create-smtp-test.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
]
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('emails', [], new ArrayList(new Email(), 10), 'Array of emails to send test email to. Maximum of 10 emails are allowed.')
->param('senderName', System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'), new Text(255, 0), 'Name of the email sender')
->param('senderEmail', System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), new Email(), 'Email of the sender')
->param('replyTo', '', new Email(), 'Reply to email', true)
->param('host', '', new HostName(), 'SMTP server host name')
->param('port', 587, new Integer(), 'SMTP server port', true)
->param('username', '', new Text(0, 0), 'SMTP server username', true)
->param('password', '', new Text(0, 0), 'SMTP server password', true)
->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', true)
->inject('response')
->inject('dbForPlatform')
->inject('queueForMails')
->inject('plan')
->action(function (string $projectId, array $emails, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForPlatform, Mail $queueForMails, array $plan) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$replyToEmail = !empty($replyTo) ? $replyTo : $senderEmail;
$subject = 'Custom SMTP email sample';
$template = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-smtp-test.tpl');
$template
->setParam('{{from}}', "{$senderName} ({$senderEmail})")
->setParam('{{replyTo}}', "{$senderName} ({$replyToEmail})")
->setParam('{{logoUrl}}', $plan['logoUrl'] ?? APP_EMAIL_LOGO_URL)
->setParam('{{accentColor}}', $plan['accentColor'] ?? APP_EMAIL_ACCENT_COLOR)
->setParam('{{twitterUrl}}', $plan['twitterUrl'] ?? APP_SOCIAL_TWITTER)
->setParam('{{discordUrl}}', $plan['discordUrl'] ?? APP_SOCIAL_DISCORD)
->setParam('{{githubUrl}}', $plan['githubUrl'] ?? APP_SOCIAL_GITHUB_APPWRITE)
->setParam('{{termsUrl}}', $plan['termsUrl'] ?? APP_EMAIL_TERMS_URL)
->setParam('{{privacyUrl}}', $plan['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL);
foreach ($emails as $email) {
$queueForMails
->setSmtpHost($host)
->setSmtpPort($port)
->setSmtpUsername($username)
->setSmtpPassword($password)
->setSmtpSecure($secure)
->setSmtpReplyTo($replyTo)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName)
->setRecipient($email)
->setName('')
->setBodyTemplate(__DIR__ . '/../../config/locale/templates/email-base-styled.tpl')
->setBody($template->render())
->setVariables([])
->setSubject($subject)
->trigger();
}
$response->noContent();
});
Http::get('/v1/projects/:projectId/templates/email')
->alias('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Get custom email template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'templates',
name: 'getEmailTemplate',
description: '/docs/references/projects/get-email-template.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EMAIL_TEMPLATE,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
->inject('response')
->inject('dbForPlatform')
->inject('locale')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform, Locale $localeObject) {
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$template = $templates['email.' . $type . '-' . $locale] ?? null;
$localeObj = new Locale($locale);
$localeObj->setFallback(System::getEnv('_APP_LOCALE', 'en'));
if (is_null($template)) {
/**
* different templates, different placeholders.
*/
$templateConfigs = [
'magicSession' => [
'file' => 'email-magic-url.tpl',
'placeholders' => ['optionButton', 'buttonText', 'optionUrl', 'clientInfo', 'securityPhrase']
],
'mfaChallenge' => [
'file' => 'email-mfa-challenge.tpl',
'placeholders' => ['description', 'clientInfo']
],
'otpSession' => [
'file' => 'email-otp.tpl',
'placeholders' => ['description', 'clientInfo', 'securityPhrase']
],
'sessionAlert' => [
'file' => 'email-session-alert.tpl',
'placeholders' => ['body', 'listDevice', 'listIpAddress', 'listCountry', 'footer']
],
];
// fallback to the base template.
$config = $templateConfigs[$type] ?? [
'file' => 'email-inner-base.tpl',
'placeholders' => ['buttonText', 'body', 'footer']
];
$templateString = file_get_contents(__DIR__ . '/../../config/locale/templates/' . $config['file']);
// We use `fromString` due to the replace above
$message = Template::fromString($templateString);
// Set type-specific parameters
foreach ($config['placeholders'] as $param) {
$escapeHtml = !in_array($param, ['clientInfo', 'body', 'footer', 'description']);
$message->setParam("{{{$param}}}", $localeObj->getText("emails.{$type}.{$param}"), escapeHtml: $escapeHtml);
}
$message
// common placeholders on all the templates
->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello"))
->setParam('{{thanks}}', $localeObj->getText("emails.{$type}.thanks"))
->setParam('{{signature}}', $localeObj->getText("emails.{$type}.signature"));
// `useContent: false` will strip new lines!
$message = $message->render(useContent: true);
$template = [
'message' => $message,
'subject' => $localeObj->getText('emails.' . $type . '.subject'),
'senderEmail' => '',
'senderName' => ''
];
}
$template['type'] = $type;
$template['locale'] = $locale;
$response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE);
});
Http::patch('/v1/projects/:projectId/templates/email')
->alias('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Update custom email templates')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'templates',
name: 'updateEmailTemplate',
description: '/docs/references/projects/update-email-template.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EMAIL_TEMPLATE,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
->param('subject', '', new Text(255), 'Email Subject')
->param('message', '', new Text(0), 'Template message')
->param('senderName', '', new Text(255, 0), 'Name of the email sender', true)
->param('senderEmail', '', new Email(), 'Email of the sender', true)
->param('replyTo', '', new Email(), 'Reply to email', true)
->inject('response')
->inject('dbForPlatform')
->inject('locale')
->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForPlatform, Locale $localeObject) {
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$templates['email.' . $type . '-' . $locale] = [
'senderName' => $senderName,
'senderEmail' => $senderEmail,
'subject' => $subject,
'replyTo' => $replyTo,
'message' => $message
];
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
$response->dynamic(new Document([
'type' => $type,
'locale' => $locale,
'senderName' => $senderName,
'senderEmail' => $senderEmail,
'subject' => $subject,
'replyTo' => $replyTo,
'message' => $message
]), Response::MODEL_EMAIL_TEMPLATE);
});
// Backwards compatibility
Http::delete('/v1/projects/:projectId/templates/email')
->alias('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Delete custom email template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'templates',
name: 'deleteEmailTemplate',
description: '/docs/references/projects/delete-email-template.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EMAIL_TEMPLATE,
)
],
contentType: ContentType::JSON
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
->inject('response')
->inject('dbForPlatform')
->inject('locale')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform, Locale $localeObject) {
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$template = $templates['email.' . $type . '-' . $locale] ?? null;
if (is_null($template)) {
throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION);
}
unset($templates['email.' . $type . '-' . $locale]);
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
$response->dynamic(new Document([
'type' => $type,
'locale' => $locale,
'senderName' => $template['senderName'],
'senderEmail' => $template['senderEmail'],
'subject' => $template['subject'],
'replyTo' => $template['replyTo'],
'message' => $template['message']
]), Response::MODEL_EMAIL_TEMPLATE);
});
Http::patch('/v1/projects/:projectId/auth/session-invalidation')
->desc('Update invalidate session option of the project')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'updateSessionInvalidation',
description: '/docs/references/projects/update-session-invalidation.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'])
->param('enabled', false, new Boolean(), 'Update authentication session invalidation status. Use this endpoint to enable or disable session invalidation on password change')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['invalidateSessions'] = $enabled;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
$response->noContent();
});
+5 -10
View File
@@ -1535,7 +1535,7 @@ Http::patch('/v1/users/:userId/email')
Query::equal('identifier', [$email]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
}
@@ -1595,9 +1595,6 @@ Http::patch('/v1/users/:userId/email')
'emailIsDisposable' => $user->getAttribute('emailIsDisposable'),
'emailIsFree' => $user->getAttribute('emailIsFree'),
]));
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
@@ -1681,7 +1678,7 @@ Http::patch('/v1/users/:userId/phone')
Query::equal('identifier', [$number]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
}
@@ -1691,9 +1688,6 @@ Http::patch('/v1/users/:userId/phone')
'phone' => $phoneValue,
'phoneVerification' => $user->getAttribute('phoneVerification'),
]));
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
@@ -2252,8 +2246,8 @@ Http::delete('/v1/users/:userId/mfa/authenticators/:type')
->label('event', 'users.[userId].delete.mfa')
->label('scope', 'users.write')
->label('audits.event', 'user.update')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('audits.resource', 'user/{request.userId}')
->label('audits.userId', '{request.userId}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk', [
new Method(
@@ -2842,6 +2836,7 @@ Http::get('/v1/users/usage')
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
default => throw new \LogicException('Unsupported period: ' . $days['period']),
};
foreach ($metrics as $metric) {
+54 -43
View File
@@ -26,6 +26,7 @@ use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
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\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -34,6 +35,7 @@ use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
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\View;
use Executor\Executor;
use MaxMind\Db\Reader;
@@ -52,8 +54,6 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
use Utopia\Http\Http;
use Utopia\Http\Route;
use Utopia\Http\Router;
use Utopia\Locale\Locale;
use Utopia\Logger\Adapter\Sentry;
use Utopia\Logger\Log;
@@ -69,7 +69,7 @@ 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, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname() ?? '';
$host = $request->getHostname();
if (!empty($previewHostname)) {
$host = $previewHostname;
}
@@ -202,12 +202,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
}
if ($deployment->getAttribute('resourceType', '') === 'functions') {
$type = 'function';
} elseif ($deployment->getAttribute('resourceType', '') === 'sites') {
$type = 'site';
}
if ($deployment->isEmpty()) {
$resourceType = $rule->getAttribute('deploymentResourceType', '');
$resourceId = $rule->getAttribute('deploymentResourceId', '');
@@ -217,6 +211,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
throw $exception;
}
if ($deployment->getAttribute('resourceType', '') === 'functions') {
$type = 'function';
} elseif ($deployment->getAttribute('resourceType', '') === 'sites') {
$type = 'site';
} else {
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown deployment resource type', view: $errorView);
}
$resource = $type === 'function' ?
$authorization->skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
$authorization->skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
@@ -304,13 +306,13 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
}
$body = $swooleRequest->getContent() ?? '';
$body = $swooleRequest->getContent() ?: '';
$method = $swooleRequest->server['request_method'];
$requestHeaders = $request->getHeaders();
if ($resource->isEmpty() || !$resource->getAttribute('enabled')) {
if ($type === 'functions') {
if ($type === 'function') {
throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND, view: $errorView);
} else {
throw new AppwriteException(AppwriteException::SITE_NOT_FOUND, view: $errorView);
@@ -332,7 +334,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$runtime = match ($type) {
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
'site' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null,
default => null
};
// Static site enforced runtime
@@ -461,10 +462,10 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
// V2 vars
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'],
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'],
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt']
]);
}
@@ -680,9 +681,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if (\is_string($logs) && \strlen($logs) > $maxLogLength) {
$warningMessage = "[WARNING] Logs truncated. The output exceeded {$maxLogLength} characters.\n";
$warningLength = \strlen($warningMessage);
$maxContentLength = max(0, $maxLogLength - $warningLength);
$logs = $warningMessage . ($maxContentLength > 0 ? \substr($logs, -$maxContentLength) : '');
$maxContentLength = $maxLogLength - \strlen($warningMessage);
$logs = $warningMessage . \substr($logs, -$maxContentLength);
}
// Truncate errors if they exceed the limit
@@ -691,9 +691,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if (\is_string($errors) && \strlen($errors) > $maxErrorLength) {
$warningMessage = "[WARNING] Errors truncated. The output exceeded {$maxErrorLength} characters.\n";
$warningLength = \strlen($warningMessage);
$maxContentLength = max(0, $maxErrorLength - $warningLength);
$errors = $warningMessage . ($maxContentLength > 0 ? \substr($errors, -$maxContentLength) : '');
$maxContentLength = $maxErrorLength - \strlen($warningMessage);
$errors = $warningMessage . \substr($errors, -$maxContentLength);
}
/** Update execution status */
$status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed';
@@ -721,14 +720,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
throw $th;
}
} finally {
if ($type === 'function' || $type === 'site') {
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
spec: $spec,
resource: $resource->getArrayCopy(),
));
}
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
spec: $spec,
resource: $resource->getArrayCopy(),
));
}
$execution->setAttribute('logs', '');
@@ -850,21 +847,23 @@ Http::init()
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->inject('route')
->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, ?Route $route) {
->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
*/
$hostname = $request->getHostname() ?? '';
$hostname = $request->getHostname();
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount);
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->getRoute();
$request->setRoute($route);
if ($route === null) {
@@ -897,6 +896,9 @@ Http::init()
if (version_compare($requestFormat, '1.9.1', '<')) {
$request->addFilter(new RequestV22());
}
if (version_compare($requestFormat, '1.9.2', '<')) {
$request->addFilter(new RequestV23());
}
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -921,6 +923,9 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.9.2', '<')) {
$response->addFilter(new ResponseV23());
}
if (version_compare($responseFormat, '1.9.1', '<')) {
$response->addFilter(new ResponseV22());
}
@@ -1142,7 +1147,9 @@ Http::options()
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount);
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);
}
}
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
@@ -1174,9 +1181,9 @@ Http::error()
->inject('bus')
->inject('devKey')
->inject('authorization')
->inject('route')
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization, ?Route $route) {
->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->getRoute();
$class = \get_class($error);
$code = $error->getCode();
$message = $error->getMessage();
@@ -1497,9 +1504,9 @@ Http::error()
->setParam('development', Http::isDevelopment())
->setParam('projectName', $project->getAttribute('name'))
->setParam('projectURL', $project->getAttribute('url'))
->setParam('message', $output['message'] ?? '')
->setParam('type', $output['type'] ?? '')
->setParam('code', $output['code'] ?? '')
->setParam('message', $output['message'])
->setParam('type', $output['type'])
->setParam('code', $output['code'])
->setParam('trace', $output['trace'] ?? [])
->setParam('exception', $error);
@@ -1541,7 +1548,9 @@ Http::get('/robots.txt')
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount);
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);
}
}
});
@@ -1573,7 +1582,9 @@ Http::get('/humans.txt')
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount);
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);
}
}
});
@@ -1610,7 +1621,7 @@ Http::get('/.well-known/acme-challenge/*')
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, 'Unknown path');
}
if (!\substr($absolute, 0, \strlen($base)) === $base) {
if (\substr($absolute, 0, \strlen($base)) !== $base) {
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, 'Invalid path');
}
@@ -1689,7 +1700,7 @@ Http::get('/_appwrite/authorize')
->inject('previewHostname')
->action(function (Request $request, Response $response, string $previewHostname) {
$host = $request->getHostname() ?? '';
$host = $request->getHostname();
if (!empty($previewHostname)) {
$host = $previewHostname;
}
+4 -4
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;
@@ -252,7 +251,7 @@ Http::get('/v1/mock/github/callback')
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$owner = $github->getOwnerName($providerInstallationId);
$projectInternalId = $project->getSequence();
@@ -284,12 +283,13 @@ Http::get('/v1/mock/github/callback')
Http::shutdown()
->groups(['mock'])
->inject('utopia')
->inject('response')
->inject('request')
->inject('route')
->action(function (Response $response, Request $request, ?Route $route) {
->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) : [];
+21 -25
View File
@@ -39,8 +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\Http\Router;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
@@ -48,7 +46,7 @@ use Utopia\Validator\WhiteList;
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user) {
preg_match_all('/{(.*?)}/', $label, $matches);
foreach ($matches[1] ?? [] as $pos => $match) {
foreach ($matches[1] as $pos => $match) {
$find = $matches[0][$pos];
$parts = explode('.', $match);
@@ -56,8 +54,8 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
}
$namespace = $parts[0] ?? '';
$replace = $parts[1] ?? '';
$namespace = $parts[0];
$replace = $parts[1];
$params = match ($namespace) {
'user' => (array) $user,
@@ -101,8 +99,8 @@ Http::init()
->inject('team')
->inject('apiKey')
->inject('authorization')
->inject('route')
->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 $route) {
->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);
}
@@ -265,8 +263,7 @@ Http::init()
$userClone->setAttribute('type', match ($apiKey->getType()) {
API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT,
API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT,
API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION,
default => ACTIVITY_TYPE_KEY_PROJECT,
default => ACTIVITY_TYPE_KEY_ORGANIZATION,
});
$auditContext->user = $userClone;
}
@@ -387,7 +384,7 @@ Http::init()
}
// Step 6: Update project and user last activity
if (! $project->isEmpty() && $project->getId() !== 'console') {
if ($project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
@@ -417,9 +414,6 @@ Http::init()
}
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
/**
* @var ?Method $method
*/
$method = $route->getLabel('sdk', false);
// Take the first method if there's more than one,
@@ -507,17 +501,17 @@ Http::init()
->inject('telemetry')
->inject('platform')
->inject('authorization')
->inject('route')
->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, ?Route $route) {
->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);
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
$path = $route->getPath();
$path = $route->getMatchedPath();
$databaseType = match (true) {
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
@@ -633,6 +627,7 @@ Http::init()
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
if ($useCache) {
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
@@ -647,7 +642,7 @@ Http::init()
if (! empty($data) && ! $cacheLog->isEmpty()) {
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
$type = $parts[0] ?? null;
$type = $parts[0];
if ($type === 'bucket' && (! $isImageTransformation || ! $isDisabled)) {
$bucketId = $parts[1] ?? null;
@@ -758,7 +753,12 @@ Http::shutdown()
->inject('project')
->inject('dbForProject')
->action(function (Http $utopia, Request $request, Response $response, Document $project, Database $dbForProject) {
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT;
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? 0;
if ($sessionLimit === 0) {
return;
}
$session = $response->getPayload();
$userId = $session['userId'] ?? '';
if (empty($userId)) {
@@ -810,8 +810,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
->inject('route')
->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, ?Route $route) 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();
@@ -861,10 +860,7 @@ Http::shutdown()
}
}
if ($route === null) {
return;
}
$route = $utopia->getRoute();
$requestParams = $route->getParamsValues();
/**
@@ -942,7 +938,7 @@ Http::shutdown()
}
$auditUser = $auditContext->user;
if (! empty($auditContext->resource) && ! \is_null($auditUser) && ! $auditUser->isEmpty()) {
if (! empty($auditContext->resource) && ! $auditUser->isEmpty()) {
/**
* audits.payload is switched to default true
* in order to auto audit payload for all endpoints
+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('utopia')
->inject('request')
->inject('project')
->inject('geodb')
->inject('user')
->inject('authorization')
->inject('route')
->action(function (Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization, ?Route $route) {
->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,6 +49,8 @@ Http::init()
}
}
$route = $utopia->match($request);
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
$isAppUser = $user->isApp($authorization->getRoles());
@@ -58,7 +59,7 @@ Http::init()
}
$auths = $project->getAttribute('auths', []);
switch ($route?->getLabel('auth.type', '')) {
switch ($route->getLabel('auth.type', '')) {
case 'email-password':
if (($auths[Config::getParam('auth')['email-password']['key']] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Email / Password authentication is disabled for this project');
+2 -10
View File
@@ -545,11 +545,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
$app->run($request, $response);
try {
$route = $app->getResource('routeMatch')?->route;
} catch (\Throwable) {
$route = null;
}
$route = $app->getRoute();
Span::add('http.path', $route?->getPath() ?? 'unknown');
} catch (\Throwable $th) {
Span::error($th);
@@ -565,11 +561,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
// All good, user is optional information for logger
}
try {
$route = $app->getResource('routeMatch')?->route;
} catch (\Throwable) {
$route = null;
}
$route = $app->getRoute();
$log = $app->getResource("log");
+4 -6
View File
@@ -1,6 +1,7 @@
<?php
use Appwrite\Platform\Modules\Compute\Specification;
use Utopia\System\System;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
@@ -24,9 +25,6 @@ const APP_MODE_ADMIN = 'admin';
const APP_PAGING_LIMIT = 12;
const APP_LIMIT_COUNT = 5000;
const APP_LIMIT_USERS = 10_000;
const APP_LIMIT_USER_PASSWORD_HISTORY = 20;
const APP_LIMIT_USER_SESSIONS_MAX = 100;
const APP_LIMIT_USER_SESSIONS_DEFAULT = 10;
const APP_LIMIT_ANTIVIRUS = 20_000_000; //20MB
const APP_LIMIT_ENCRYPTION = 20_000_000; //20MB
const APP_LIMIT_COMPRESSION = 20_000_000; //20MB
@@ -46,8 +44,8 @@ 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 = 4322;
const APP_VERSION_STABLE = '1.9.1';
const APP_CACHE_BUSTER = 4323;
const APP_VERSION_STABLE = '1.9.2';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@@ -190,7 +188,7 @@ const BUILD_TYPE_RETRY = 'retry';
// Deletion Types
const ENABLE_EXECUTIONS_LIMIT_ON_ROUTE = false;
\define('ENABLE_EXECUTIONS_LIMIT_ON_ROUTE', System::getEnv('_APP_EXECUTIONS_LIMIT_ON_ROUTE', 'disabled') === 'enabled');
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
+2 -2
View File
@@ -85,7 +85,7 @@ return function (Container $container): void {
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]) ?? new Document();
]);
});
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
@@ -139,7 +139,7 @@ return function (Container $container): void {
$sdkValidator = new WhiteList($servers, true);
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
if ($sdk !== 'unknown' && $sdkValidator->isValid($sdk)) {
$sdks = $key->getAttribute('sdks', []);
if (!\in_array($sdk, $sdks, true)) {
+6 -7
View File
@@ -71,7 +71,7 @@ $register->set('logger', function () {
$providerConfig = match ($providerName) {
'sentry' => [ 'key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => '',],
'logowl' => ['ticket' => $configChunks[0] ?? '', 'host' => ''],
'logowl' => ['ticket' => $configChunks[0], 'host' => ''],
default => ['key' => $providerConfig],
};
}
@@ -249,11 +249,11 @@ $register->set('pools', function () {
$poolSize = max(1, (int)($instanceConnections / $workerCount));
foreach ($connections as $key => $connection) {
$type = $connection['type'] ?? '';
$multiple = $connection['multiple'] ?? false;
$schemes = $connection['schemes'] ?? [];
$type = $connection['type'];
$multiple = $connection['multiple'];
$schemes = $connection['schemes'];
$config = [];
$dsns = explode(',', $connection['dsns'] ?? '');
$dsns = explode(',', $connection['dsns']);
foreach ($dsns as &$dsn) {
$dsn = explode('=', $dsn);
$name = ($multiple) ? $key . '_' . $dsn[0] : $key;
@@ -318,7 +318,7 @@ $register->set('pools', function () {
));
});
},
'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) {
default => function () use ($dsnHost, $dsnPort, $dsnPass) {
$redis = new \Redis();
@$redis->pconnect($dsnHost, (int)$dsnPort);
if ($dsnPass) {
@@ -328,7 +328,6 @@ $register->set('pools', function () {
return $redis;
},
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Invalid scheme'),
};
$poolAdapter = System::getEnv('_APP_POOL_ADAPTER', default: 'stack') === 'swoole' ? new SwoolePool() : new StackPool();
+1 -1
View File
@@ -266,7 +266,7 @@ function getDevice(string $root, string $connection = ''): Device
return new Local($root);
}
} else {
switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) {
switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL))) {
case Storage::DEVICE_LOCAL:
default:
return new Local($root);
+9 -18
View File
@@ -47,7 +47,6 @@ use Utopia\DI\Container;
use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
use Utopia\Http\Http;
use Utopia\Http\Router;
use Utopia\Locale\Locale;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
@@ -376,7 +375,7 @@ return function (Container $container): void {
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]) ?? new Document();
]);
});
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
@@ -479,14 +478,10 @@ return function (Container $container): void {
}
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
if ($response) { // if in http context - add debug header
$response->addHeader('X-Debug-Fallback', 'false');
}
$response->addHeader('X-Debug-Fallback', 'false');
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
if ($response) {
$response->addHeader('X-Debug-Fallback', 'true');
}
$response->addHeader('X-Debug-Fallback', 'true');
$fallback = $request->getHeader('x-fallback-cookies', '');
$fallback = \json_decode($fallback, true);
$store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : ''));
@@ -621,12 +616,10 @@ return function (Container $container): 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/';
$urlPath = \parse_url($request->getURI(), PHP_URL_PATH);
$urlPath = \is_string($urlPath) ? ($urlPath === '' ? '/' : $urlPath) : '/';
$match = Router::matchRoute($request->getMethod(), $urlPath);
if ($match !== null) {
$route = $utopia->match($request);
if (!empty($route)) {
$isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) &&
!\str_starts_with($match->route->getPath(), $deprecatedProjectPathPrefix);
!\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix);
if ($isDeprecatedAlias) {
$projectId = \explode('/', $request->getURI(), 5)[3] ?? '';
@@ -1087,7 +1080,7 @@ return function (Container $container): void {
$sdkValidator = new WhiteList($servers, true);
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
if ($sdk !== 'unknown' && $sdkValidator->isValid($sdk)) {
$sdks = $key->getAttribute('sdks', []);
if (! in_array($sdk, $sdks)) {
@@ -1112,10 +1105,8 @@ return function (Container $container): void {
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
} else {
$urlPath = \parse_url($request->getURI(), PHP_URL_PATH);
$urlPath = \is_string($urlPath) ? ($urlPath === '' ? '/' : $urlPath) : '/';
$match = Router::matchRoute($request->getMethod(), $urlPath);
$path = $match !== null ? $match->route->getPath() : $request->getURI();
$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')) {
$uri = $request->getURI();
+1 -1
View File
@@ -368,7 +368,7 @@ return function (Container $container): void {
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', \get_class($error));
$log->addTag('projectId', $project->getId() ?? '');
$log->addTag('projectId', $project->getId());
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
+3 -1
View File
@@ -262,7 +262,9 @@ $stats->create();
$containerId = uniqid();
$statsDocument = null;
$workerNumber = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$workerNumber = intval(System::getEnv('_APP_WORKERS_NUM', 0))
?: intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$adapter = new Adapter\Swoole(port: System::getEnv('PORT', 80));
$adapter
+1 -1
View File
@@ -129,7 +129,7 @@ $worker
$log->setAction('appwrite-queue-' . $queueName);
$log->addTag('verboseType', get_class($error));
$log->addTag('code', $error->getCode());
$log->addTag('projectId', $project->getId() ?? 'n/a');
$log->addTag('projectId', $project->getId());
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
+1 -1
View File
@@ -67,7 +67,7 @@
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.6.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/http": "dev-refactor/coroutine-safe-dispatch as 0.34.99",
"utopia-php/http": "0.34.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
Generated
+20 -31
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "847532ec8f650852450fec3d6e806f4e",
"content-hash": "c5ae97637fd0ec0a950044d1c33677ea",
"packages": [
{
"name": "adhocore/jwt",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.21",
"version": "5.3.22",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d"
"reference": "d765945da6b3141852014b2f96ecf1fe7e3d6ba7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
"url": "https://api.github.com/repos/utopia-php/database/zipball/d765945da6b3141852014b2f96ecf1fe7e3d6ba7",
"reference": "d765945da6b3141852014b2f96ecf1fe7e3d6ba7",
"shasum": ""
},
"require": {
@@ -3903,9 +3903,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.3.21"
"source": "https://github.com/utopia-php/database/tree/5.3.22"
},
"time": "2026-04-10T12:38:57+00:00"
"time": "2026-04-20T07:12:46+00:00"
},
{
"name": "utopia-php/detector",
@@ -4271,20 +4271,21 @@
},
{
"name": "utopia-php/http",
"version": "dev-refactor/coroutine-safe-dispatch",
"version": "0.34.21",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "07087af8f0e8976a143507e3fd8123ae065cc91a"
"reference": "49a6bd3ea0d2966aa19cf707255d442675288a24"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/07087af8f0e8976a143507e3fd8123ae065cc91a",
"reference": "07087af8f0e8976a143507e3fd8123ae065cc91a",
"url": "https://api.github.com/repos/utopia-php/http/zipball/49a6bd3ea0d2966aa19cf707255d442675288a24",
"reference": "49a6bd3ea0d2966aa19cf707255d442675288a24",
"shasum": ""
},
"require": {
"php": ">=8.3",
"ext-swoole": "*",
"php": ">=8.2",
"utopia-php/compression": "0.1.*",
"utopia-php/di": "0.3.*",
"utopia-php/servers": "0.3.*",
@@ -4294,14 +4295,11 @@
"require-dev": {
"doctrine/instantiator": "^1.5",
"laravel/pint": "1.*",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.0",
"rector/rector": "^2.4",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "1.*",
"phpunit/phpunit": "^9.5.25",
"swoole/ide-helper": "4.8.3"
},
"suggest": {
"ext-swoole": "Required to use the Swoole server adapter (\\Utopia\\Http\\Adapter\\Swoole\\Server)."
},
"type": "library",
"autoload": {
"psr-4": {
@@ -4321,9 +4319,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/refactor/coroutine-safe-dispatch"
"source": "https://github.com/utopia-php/http/tree/0.34.21"
},
"time": "2026-04-24T15:49:39+00:00"
"time": "2026-04-19T19:44:04+00:00"
},
{
"name": "utopia-php/image",
@@ -8443,18 +8441,9 @@
"time": "2024-11-07T12:36:22+00:00"
}
],
"aliases": [
{
"package": "utopia-php/http",
"version": "dev-refactor/coroutine-safe-dispatch",
"alias": "0.34.99",
"alias_normalized": "0.34.99.0"
}
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"utopia-php/http": 20
},
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
+1 -1
View File
@@ -253,7 +253,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:7.8.26
image: appwrite/console:7.8.45
restart: unless-stopped
networks:
- appwrite
@@ -1 +0,0 @@
Initialize an MFA challenge of the specified factor. The factor must be available on the account.
@@ -1 +0,0 @@
Use this endpoint to log out the currently logged in user from their account. When successful this endpoint will delete the user session and remove the session secret cookie from the user client.
@@ -1 +0,0 @@
Get the collection activity logs list by its unique ID.
@@ -1 +0,0 @@
Get the document activity logs list by its unique ID.
@@ -1 +0,0 @@
List attributes in the collection.
@@ -1 +0,0 @@
Create a new build for an existing function deployment. This endpoint allows you to rebuild a deployment with the updated function configuration, including its entrypoint and build commands if they have been modified. The build process will be queued and executed asynchronously. The original deployment's code will be preserved and used for the new build.
@@ -1,5 +0,0 @@
Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.
This endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https://appwrite.io/docs/functions).
Use the "command" param to set the entrypoint used to execute your code.
@@ -1 +0,0 @@
Trigger a function execution. The returned object will return you the current execution status. You can ping the `Get Execution` endpoint to get updates on the current execution status. Once this endpoint is called, your function execution process will start asynchronously.
@@ -1 +0,0 @@
Create a new function. You can pass a list of [permissions](https://appwrite.io/docs/permissions) to allow different project users or team with access to execute the function using the client API.
@@ -1 +0,0 @@
Create a new function environment variable. These variables can be accessed in the function at runtime as environment variables.
@@ -1 +0,0 @@
Delete a code deployment by its unique ID.
@@ -1 +0,0 @@
Delete a function execution by its unique ID.
@@ -1 +0,0 @@
Delete a function by its unique ID.
@@ -1 +0,0 @@
Delete a variable by its unique ID.
@@ -1 +0,0 @@
Get a Deployment's contents by its unique ID. This endpoint supports range requests for partial or streaming file download.
@@ -1 +0,0 @@
Get a code deployment by its unique ID.
@@ -1 +0,0 @@
Get a function execution log by its unique ID.
@@ -1 +0,0 @@
Get usage metrics and statistics for a for a specific function. View statistics including total deployments, builds, executions, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
@@ -1 +0,0 @@
Get a function by its unique ID.
@@ -1 +0,0 @@
Get usage metrics and statistics for a for all functions. View statistics including total functions, deployments, builds, executions, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
@@ -1 +0,0 @@
Get a function template using ID. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
@@ -1 +0,0 @@
Get a variable by its unique ID.
@@ -1 +0,0 @@
Get a list of all the function's code deployments. You can use the query params to filter your results.
@@ -1 +0,0 @@
Get a list of all the current user function execution logs. You can use the query params to filter your results.
@@ -1 +0,0 @@
Get a list of all the project's functions. You can use the query params to filter your results.
@@ -1 +0,0 @@
Get a list of all runtimes that are currently active on your instance.
@@ -1 +0,0 @@
List allowed function specifications for this instance.
@@ -1 +0,0 @@
List available function templates. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
@@ -1 +0,0 @@
Get a list of all variables of a specific function.
@@ -1 +0,0 @@
Cancel an ongoing function deployment build. If the build is already in progress, it will be stopped and marked as canceled. If the build hasn't started yet, it will be marked as canceled without executing. You cannot cancel builds that have already completed (status 'ready') or failed. The response includes the final build status and details.
@@ -1 +0,0 @@
Update the function code deployment ID using the unique function ID. Use this endpoint to switch the code deployment that should be executed by the execution endpoint.
@@ -1 +0,0 @@
Update function by its unique ID.
@@ -1 +0,0 @@
Update variable by its unique ID.
@@ -1 +0,0 @@
Get the number of projects containing metrics that are waiting to be processed in the Appwrite internal queue server.
@@ -1 +0,0 @@
Get the number of tasks that are waiting to be processed in the Appwrite internal queue server.
-1
View File
@@ -1 +0,0 @@
Check the Appwrite queue messaging servers are up and connection is successful.
-1
View File
@@ -1 +0,0 @@
Delete a message by its unique ID.
@@ -1 +0,0 @@
Create a new project variable. This variable will be accessible in all Appwrite Functions at runtime.
@@ -1 +0,0 @@
Delete a project variable by its unique ID.
-1
View File
@@ -1 +0,0 @@
Get a project variable by its unique ID.
@@ -1 +0,0 @@
Get a list of all project variables. These variables will be accessible in all Appwrite Functions at runtime.
@@ -1 +0,0 @@
Update project variable by its unique ID. This variable will be accessible in all Appwrite Functions at runtime.
-1
View File
@@ -1 +0,0 @@
Create a new API key. It's recommended to have multiple API keys with strict scopes for separate functions within your project.
@@ -1 +0,0 @@
Create a new platform for your project. Use this endpoint to register a new platform where your users will run your application which will interact with the Appwrite API.
@@ -1 +0,0 @@
Create a new webhook. Use this endpoint to configure a URL that will receive events from Appwrite when specific events occur.
-1
View File
@@ -1 +0,0 @@
Delete a key by its unique ID. Once deleted, the key can no longer be used to authenticate API calls.
@@ -1 +0,0 @@
Delete a platform by its unique ID. This endpoint removes the platform and all its configurations from the project.
@@ -1 +0,0 @@
Delete a webhook by its unique ID. Once deleted, the webhook will no longer receive project events.
-1
View File
@@ -1 +0,0 @@
Get a key by its unique ID. This endpoint returns details about a specific API key in your project including it's scopes.
-1
View File
@@ -1 +0,0 @@
Get a platform by its unique ID. This endpoint returns the platform's details, including its name, type, and key configurations.
-1
View File
@@ -1 +0,0 @@
Get a webhook by its unique ID. This endpoint returns details about a specific webhook configured for a project.
-1
View File
@@ -1 +0,0 @@
Get a list of all API keys from the current project.
@@ -1 +0,0 @@
Get a list of all platforms in the project. This endpoint returns an array of all platforms and their configurations.
@@ -1 +0,0 @@
Get a list of all webhooks belonging to the project. You can use the query params to filter your results.
@@ -1 +0,0 @@
Update the status of all API types. Use this endpoint to enable or disable API types such as REST, GraphQL and Realtime all at once.
@@ -1 +0,0 @@
Update the status of a specific API type. Use this endpoint to enable or disable API types such as REST, GraphQL and Realtime.
@@ -1 +0,0 @@
Update how long sessions created within a project should stay active for.
@@ -1 +0,0 @@
Update the maximum number of users allowed in this project. Set to 0 for unlimited users.
@@ -1 +0,0 @@
Enable or disable checking user passwords against common passwords dictionary. This helps ensure users don't use common and insecure passwords.
@@ -1 +0,0 @@
Update the authentication password history requirement. Use this endpoint to require new passwords to be different than the last X amount of previously used ones.
@@ -1 +0,0 @@
Update the maximum number of sessions allowed per user within the project, if the limit is hit the oldest session will be deleted to make room for new sessions.
-1
View File
@@ -1 +0,0 @@
Update a key by its unique ID. Use this endpoint to update the name, scopes, or expiration time of an API key.
@@ -1 +0,0 @@
Update project membership privacy settings. Use this endpoint to control what user information is visible to other team members, such as user name, email, and MFA status.
@@ -1 +0,0 @@
Enable or disable checking user passwords against their personal data. This helps prevent users from using personal information in their passwords.
@@ -1 +0,0 @@
Update a platform by its unique ID. Use this endpoint to update the platform's name, key, platform store ID, or hostname.
@@ -1 +0,0 @@
Update the status of all services. Use this endpoint to enable or disable all optional services at once.
@@ -1 +0,0 @@
Update the status of a specific service. Use this endpoint to enable or disable a service in your project.
@@ -1 +0,0 @@
Enable or disable session email alerts. When enabled, users will receive email notifications when new sessions are created.
@@ -1 +0,0 @@
Invalidate all existing sessions. An optional auth security setting for projects, and enabled by default for console project.
@@ -1 +0,0 @@
Update the webhook signature key. This endpoint can be used to regenerate the signature key used to sign and validate payload deliveries for a specific webhook.
@@ -1 +0,0 @@
Update a webhook by its unique ID. Use this endpoint to update the URL, events, or status of an existing webhook.
-1
View File
@@ -1 +0,0 @@
Get a database by its unique ID. This endpoint response returns a JSON object with the database metadata.
@@ -1 +0,0 @@
Decrement a specific column of a row by a given value.

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