mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
218
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57041feefd | ||
|
|
b1d37bc4be | ||
|
|
4808cad081 | ||
|
|
72cd7671c4 | ||
|
|
c3e411fcaa | ||
|
|
8c31e9f206 | ||
|
|
a4ad1b6df3 | ||
|
|
9e94f15f02 | ||
|
|
e27719108b | ||
|
|
8bf2f54a51 | ||
|
|
8c03db70e9 | ||
|
|
8ea69e0321 | ||
|
|
dfec2b3cb7 | ||
|
|
848f09956e | ||
|
|
51f50b161c | ||
|
|
52e3319a86 | ||
|
|
8b41aed919 | ||
|
|
ba4430801d | ||
|
|
1c1ec43150 | ||
|
|
2f62cced0a | ||
|
|
afb8f70316 | ||
|
|
2097b0a0b0 | ||
|
|
78ef52cc9e | ||
|
|
f040a4dc31 | ||
|
|
56385ce167 | ||
|
|
bc592903db | ||
|
|
06a2d48deb | ||
|
|
c4d9c3dc4f | ||
|
|
9f504cd065 | ||
|
|
9dc27e9ca5 | ||
|
|
db3d00b1da | ||
|
|
5ab42b8d32 | ||
|
|
6b66923f18 | ||
|
|
56165ee3d9 | ||
|
|
69d53cb2d4 | ||
|
|
447375dcbf | ||
|
|
afab349a77 | ||
|
|
2a95cfd5a3 | ||
|
|
8a1f8c71b2 | ||
|
|
0a71efc244 | ||
|
|
fcc7a56f4a | ||
|
|
0aab3e9a43 | ||
|
|
be5eeb1aba | ||
|
|
08b43dce50 | ||
|
|
ad3bdee6c1 | ||
|
|
e00efaea68 | ||
|
|
be214f7968 | ||
|
|
64d182ac6a | ||
|
|
bb38bf4248 | ||
|
|
956285d522 | ||
|
|
dc704fdb51 | ||
|
|
e388d2f6a3 | ||
|
|
fc62ef2fcc | ||
|
|
b01ec03723 | ||
|
|
489b2c4e21 | ||
|
|
1a46fc2006 | ||
|
|
c1b7aff2d9 | ||
|
|
7df1814203 | ||
|
|
8f39783d7a | ||
|
|
4043153df3 | ||
|
|
9765c7f0e3 | ||
|
|
eddd159af8 | ||
|
|
17de886296 | ||
|
|
27b0e48296 | ||
|
|
6b2054d0b5 | ||
|
|
df0f7ba581 | ||
|
|
c484c487a9 | ||
|
|
47f3ab930b | ||
|
|
c8c3c68b0e | ||
|
|
facae65f08 | ||
|
|
ef6711e317 | ||
|
|
e06b06a21b | ||
|
|
c97dd78335 | ||
|
|
bf9bb22ac5 | ||
|
|
1b826df8f9 | ||
|
|
11f23fdcfa | ||
|
|
b5ec92964c | ||
|
|
71b74e21a3 | ||
|
|
1e797b3f01 | ||
|
|
f1b2dd7335 | ||
|
|
50c379c5c3 | ||
|
|
463e5acf50 | ||
|
|
807e8bec8b | ||
|
|
935c1e40eb | ||
|
|
e472d98fe3 | ||
|
|
7fe65eec57 | ||
|
|
4cf375de6d | ||
|
|
19d0eb66c0 | ||
|
|
05d70f8826 | ||
|
|
98ec9e45c4 | ||
|
|
6dc17c91bc | ||
|
|
1493b7b8a6 | ||
|
|
965836c8b4 | ||
|
|
4545989c91 | ||
|
|
b71d42d226 | ||
|
|
945cdb3a99 | ||
|
|
a0db023860 | ||
|
|
6a7280e7dd | ||
|
|
680cb04de7 | ||
|
|
6e50ed050d | ||
|
|
f78b5c6596 | ||
|
|
55001a7daa | ||
|
|
6d2876ab26 | ||
|
|
b510194f00 | ||
|
|
8fd1c5d620 | ||
|
|
590f063694 | ||
|
|
90e1433878 | ||
|
|
2b42487198 | ||
|
|
53ed9462bd | ||
|
|
0da185e689 | ||
|
|
dc39af50a1 | ||
|
|
6da132db46 | ||
|
|
b2a02e3807 | ||
|
|
e7f78b3e01 | ||
|
|
7376c5b517 | ||
|
|
db84c79802 | ||
|
|
6ba810a4da | ||
|
|
6d9b787816 | ||
|
|
7b8fb409b1 | ||
|
|
1fb78115e8 | ||
|
|
49c93c635d | ||
|
|
5010cdedbb | ||
|
|
13413c05c3 | ||
|
|
dc185be836 | ||
|
|
29be9b6019 | ||
|
|
1247b1c719 | ||
|
|
445e86361c | ||
|
|
80197b566c | ||
|
|
8671533878 | ||
|
|
f51f02375a | ||
|
|
d40df613de | ||
|
|
ebc3febc38 | ||
|
|
e77bfae091 | ||
|
|
f9efd803c1 | ||
|
|
54d6470163 | ||
|
|
50640b5bb9 | ||
|
|
4d4ba508ef | ||
|
|
4f2f9fedfa | ||
|
|
732538504d | ||
|
|
d3dcbfe567 | ||
|
|
905d2a8eaa | ||
|
|
b5e46c1a60 | ||
|
|
a3a8ad88fe | ||
|
|
9d6428d5d5 | ||
|
|
d7f8ca3f01 | ||
|
|
56bcc0d09f | ||
|
|
8b0c60d2f9 | ||
|
|
ffdfe4bbf8 | ||
|
|
556ca10ed9 | ||
|
|
193beb76fe | ||
|
|
8fcba8b5c9 | ||
|
|
6c8b34c230 | ||
|
|
b2884ddb88 | ||
|
|
82798fa5a3 | ||
|
|
377ade3c44 | ||
|
|
71533aaaf3 | ||
|
|
ccbe7e5d3f | ||
|
|
bea7739d7e | ||
|
|
76320d652a | ||
|
|
6d5968e2ea | ||
|
|
fd8fedca18 | ||
|
|
70c380fa36 | ||
|
|
512a7ae2bd | ||
|
|
49d3cd980f | ||
|
|
39a396810a | ||
|
|
c52ca65905 | ||
|
|
1d307178bb | ||
|
|
c3ed2ff6ce | ||
|
|
efadf17bfe | ||
|
|
bcfec8d5de | ||
|
|
4b2e22d9da | ||
|
|
45e22e2243 | ||
|
|
0a21f3b139 | ||
|
|
5d53685976 | ||
|
|
d869c39783 | ||
|
|
fe02964ebd | ||
|
|
cc8eb62c83 | ||
|
|
b1ad6ea87c | ||
|
|
d52d6c0bf0 | ||
|
|
86cfea0edb | ||
|
|
a1342b4b9d | ||
|
|
a941d8b855 | ||
|
|
d40a355d9d | ||
|
|
f4f5494b85 | ||
|
|
fc0fd2f6ac | ||
|
|
70a75c2e7b | ||
|
|
6bc2168e29 | ||
|
|
a3ba66c90a | ||
|
|
df5ccc10ad | ||
|
|
2807d6cd9a | ||
|
|
18b1c344e6 | ||
|
|
c6e32940f4 | ||
|
|
a31cb67ec5 | ||
|
|
5ecd15a5f5 | ||
|
|
ec5472f1ed | ||
|
|
9ae804f8ae | ||
|
|
0cce480592 | ||
|
|
b622b092a8 | ||
|
|
96fe989f6d | ||
|
|
0de26be6e6 | ||
|
|
dc0a5c88b7 | ||
|
|
d13dbae0fe | ||
|
|
d6f51a96a5 | ||
|
|
8442a1e612 | ||
|
|
31728c9b72 | ||
|
|
9d0fc9e5b1 | ||
|
|
a332eb5f32 | ||
|
|
cc82b1a5cf | ||
|
|
ba32012744 | ||
|
|
775d21e5ee | ||
|
|
8f6530d1e7 | ||
|
|
4297c70f58 | ||
|
|
16ed60a5c3 | ||
|
|
d831b93934 | ||
|
|
e42a26fdad | ||
|
|
2c4770d29b | ||
|
|
2a9e423cb1 | ||
|
|
14869cc6d6 |
@@ -210,7 +210,7 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
|
||||
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
|
||||
const allModes = ['dedicated', 'shared'];
|
||||
|
||||
const defaultDatabases = ['MongoDB'];
|
||||
const defaultModes = ['dedicated'];
|
||||
@@ -479,11 +479,8 @@ jobs:
|
||||
env:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -512,7 +509,7 @@ jobs:
|
||||
# Services that rely on sequential test method execution (shared static state)
|
||||
FUNCTIONAL_FLAG="--functional"
|
||||
case "${{ matrix.service }}" in
|
||||
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
|
||||
Databases|TablesDB|Functions|Realtime|GraphQL|ProjectWebhooks) FUNCTIONAL_FLAG="" ;;
|
||||
esac
|
||||
|
||||
docker compose exec -T \
|
||||
@@ -557,11 +554,8 @@ jobs:
|
||||
env:
|
||||
_APP_OPTIONS_ABUSE: enabled
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -618,11 +612,8 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -701,9 +692,8 @@ jobs:
|
||||
|
||||
- name: Installing latest version
|
||||
run: |
|
||||
rm docker-compose.yml
|
||||
# TODO: Minify docker-compose; remove development tooling
|
||||
rm .env
|
||||
curl https://appwrite.io/install/compose -o docker-compose.yml
|
||||
curl https://appwrite.io/install/env -o .env
|
||||
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
|
||||
docker compose up -d
|
||||
|
||||
+1
-1
@@ -892,7 +892,7 @@
|
||||
* Unset index length by @fogelito in https://github.com/appwrite/appwrite/pull/8978
|
||||
* Update base to 0.9.5 by @basert in https://github.com/appwrite/appwrite/pull/9005
|
||||
* Sync main into 1.6.x by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9011
|
||||
* Improved shared tables V2 by @abnegate in https://github.com/appwrite/appwrite/pull/9013
|
||||
* Improved shared tables by @abnegate in https://github.com/appwrite/appwrite/pull/9013
|
||||
* Ensure backwards compatibility for 1.6.x by @christyjacob4 in https://github.com/appwrite/appwrite/pull/9018
|
||||
|
||||
# Version 1.6.0
|
||||
|
||||
+3
-3
@@ -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 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
||||
@@ -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.
|
||||
|
||||
+5
-4
@@ -2,10 +2,10 @@
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
@@ -253,6 +253,10 @@ $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePubli
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
|
||||
@@ -263,9 +267,6 @@ $container->set('queueForFunctions', function (Publisher $publisher) {
|
||||
$container->set('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('logError', function (Registry $register) {
|
||||
return function (Throwable $error, string $namespace, string $action) use ($register) {
|
||||
Console::error('[Error] Timestamp: ' . date('c', time()));
|
||||
|
||||
@@ -9,11 +9,5 @@ return [
|
||||
'mfaChallenge',
|
||||
'sessionAlert',
|
||||
'otpSession'
|
||||
],
|
||||
'sms' => [
|
||||
'verification',
|
||||
'login',
|
||||
'invitation',
|
||||
'mfaChallenge'
|
||||
]
|
||||
];
|
||||
|
||||
@@ -55,6 +55,8 @@ $admins = [
|
||||
'tables.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
'templates.read',
|
||||
'templates.write',
|
||||
'projects.write',
|
||||
'keys.read',
|
||||
'keys.write',
|
||||
|
||||
@@ -204,4 +204,12 @@ return [ // List of publicly visible scopes
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s platforms",
|
||||
],
|
||||
"templates.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s templates",
|
||||
],
|
||||
"templates.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s templates",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -300,6 +300,26 @@ return [
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/cursor-plugin/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'claude-plugin',
|
||||
'name' => 'ClaudePlugin',
|
||||
'version' => '0.1.0',
|
||||
'url' => 'https://github.com/appwrite/claude-plugin.git',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'prism' => 'claude-plugin',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-claude-plugin'),
|
||||
'gitUrl' => 'git@github.com:appwrite/claude-plugin.git',
|
||||
'gitRepoName' => 'claude-plugin',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/claude-plugin/CHANGELOG.md'),
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -1487,13 +1487,13 @@ return [
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => 'crm-dashboard-react-admin',
|
||||
'name' => 'CRM dashboard with React Admin',
|
||||
'tagline' => 'A React-based admin dashboard template with CRM features.',
|
||||
'key' => 'dashboard-react-admin',
|
||||
'name' => 'E-commerce dashboard with React Admin',
|
||||
'tagline' => 'A React-based admin dashboard template with e-commerce features.',
|
||||
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
|
||||
'useCases' => [SiteUseCases::DASHBOARD],
|
||||
'screenshotDark' => $url . '/images/sites/templates/crm-dashboard-react-admin-dark.png',
|
||||
'screenshotLight' => $url . '/images/sites/templates/crm-dashboard-react-admin-light.png',
|
||||
'useCases' => [SiteUseCases::DASHBOARD, SiteUseCases::ECOMMERCE],
|
||||
'screenshotDark' => $url . '/images/sites/templates/dashboard-react-admin-dark.png',
|
||||
'screenshotLight' => $url . '/images/sites/templates/dashboard-react-admin-light.png',
|
||||
'frameworks' => [
|
||||
getFramework('REACT', [
|
||||
'providerRootDirectory' => './react/react-admin',
|
||||
|
||||
@@ -872,18 +872,18 @@ return [
|
||||
],
|
||||
[
|
||||
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
|
||||
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
|
||||
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
|
||||
'introduction' => '0.13.0',
|
||||
'default' => '900',
|
||||
'default' => '2700',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
|
||||
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
|
||||
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
|
||||
'introduction' => '1.7.0',
|
||||
'default' => '900',
|
||||
'default' => '2700',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
|
||||
@@ -453,7 +453,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',
|
||||
@@ -474,19 +474,26 @@ Http::delete('/v1/account')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForDeletes')
|
||||
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
|
||||
->inject('authorization')
|
||||
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes, Authorization $authorization) {
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
// get all memberships
|
||||
$memberships = $user->getAttribute('memberships', []);
|
||||
foreach ($memberships as $membership) {
|
||||
// prevent deletion if at least one active membership
|
||||
if ($membership->getAttribute('confirm', false)) {
|
||||
throw new Exception(Exception::USER_DELETION_PROHIBITED);
|
||||
if (!$membership->getAttribute('confirm', false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId'));
|
||||
if ($team->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Team is left as-is — we don't promote non-owner members to owner.
|
||||
// Orphan teams are cleaned up later by Cloud's inactive project cleanup.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2265,7 +2272,10 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
|
||||
$subject = $locale->getText("emails.magicSession.subject");
|
||||
$preview = $locale->getText("emails.magicSession.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ?? [];
|
||||
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.magicSession-' . $locale->fallback] ?? [];
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$agentOs = $detector->getOS();
|
||||
@@ -2295,8 +2305,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'])) {
|
||||
@@ -2305,8 +2315,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
|
||||
@@ -2323,8 +2338,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'] ?? '';
|
||||
@@ -2332,7 +2352,8 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
@@ -2575,7 +2596,9 @@ Http::post('/v1/account/tokens/email')
|
||||
$preview = $locale->getText("emails.otpSession.preview");
|
||||
$heading = $locale->getText("emails.otpSession.heading");
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.otpSession-' . $locale->fallback] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
@@ -2611,7 +2634,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'])) {
|
||||
@@ -2620,8 +2644,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
|
||||
@@ -2638,8 +2667,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'] ?? '';
|
||||
@@ -2647,7 +2681,8 @@ Http::post('/v1/account/tokens/email')
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
@@ -2968,11 +3003,6 @@ Http::post('/v1/account/tokens/phone')
|
||||
if ($sendSMS) {
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['sms.login-' . $locale->default] ?? [];
|
||||
if (!empty($customTemplate)) {
|
||||
$message = $customTemplate['message'] ?? $message;
|
||||
}
|
||||
|
||||
$projectName = $project->getAttribute('name');
|
||||
if ($project->getId() === 'console') {
|
||||
$projectName = $platform['platformName'];
|
||||
@@ -3726,7 +3756,9 @@ Http::post('/v1/account/recovery')
|
||||
$body = $locale->getText("emails.recovery.body");
|
||||
$subject = $locale->getText("emails.recovery.subject");
|
||||
$preview = $locale->getText("emails.recovery.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.recovery-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.recovery-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.recovery-' . $locale->fallback] ?? [];
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
@@ -3743,7 +3775,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'])) {
|
||||
@@ -3752,8 +3785,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
|
||||
@@ -3770,8 +3808,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'] ?? '';
|
||||
@@ -3779,7 +3822,8 @@ Http::post('/v1/account/recovery')
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
@@ -4034,7 +4078,9 @@ Http::post('/v1/account/verifications/email')
|
||||
$subject = $locale->getText("emails.verification.subject");
|
||||
$heading = $locale->getText("emails.verification.heading");
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.verification-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.verification-' . $locale->fallback] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
@@ -4060,7 +4106,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'])) {
|
||||
@@ -4069,8 +4116,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
|
||||
@@ -4087,8 +4139,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'] ?? '';
|
||||
@@ -4096,7 +4153,8 @@ Http::post('/v1/account/verifications/email')
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
@@ -4333,11 +4391,6 @@ Http::post('/v1/account/verifications/phone')
|
||||
if ($sendSMS) {
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['sms.verification-' . $locale->default] ?? [];
|
||||
if (!empty($customTemplate)) {
|
||||
$message = $customTemplate['message'] ?? $message;
|
||||
}
|
||||
|
||||
$messageContent = Template::fromString($locale->getText("sms.verification.body"));
|
||||
$messageContent
|
||||
->setParam('{{project}}', $project->getAttribute('name'))
|
||||
@@ -4612,7 +4665,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',
|
||||
|
||||
@@ -231,7 +231,7 @@ function execute(
|
||||
$validations = GraphQL::getStandardValidationRules();
|
||||
|
||||
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
|
||||
$validations[] = new DisableIntrospection();
|
||||
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
|
||||
}
|
||||
|
||||
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
|
||||
|
||||
@@ -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;
|
||||
@@ -619,636 +611,6 @@ 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/sms/:type/:locale')
|
||||
->desc('Get custom SMS template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'getSmsTemplate',
|
||||
description: '/docs/references/projects/get-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_TEMPLATE,
|
||||
)
|
||||
],
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'projects.getSMSTemplate',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'getSMSTemplate',
|
||||
description: '/docs/references/projects/get-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_TEMPLATE,
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? [], true), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
|
||||
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
|
||||
|
||||
if (is_null($template)) {
|
||||
$template = [
|
||||
'message' => Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl')->render(),
|
||||
];
|
||||
}
|
||||
|
||||
$template['type'] = $type;
|
||||
$template['locale'] = $locale;
|
||||
|
||||
$response->dynamic(new Document($template), Response::MODEL_SMS_TEMPLATE);
|
||||
});
|
||||
|
||||
|
||||
Http::get('/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', false, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$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/sms/:type/:locale')
|
||||
->desc('Update custom SMS template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'updateSmsTemplate',
|
||||
description: '/docs/references/projects/update-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_TEMPLATE,
|
||||
)
|
||||
],
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'projects.updateSMSTemplate',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'updateSMSTemplate',
|
||||
description: '/docs/references/projects/update-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_TEMPLATE,
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? [], true), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->param('message', '', new Text(0), 'Template message')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForPlatform) {
|
||||
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$templates['sms.' . $type . '-' . $locale] = [
|
||||
'message' => $message
|
||||
];
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'message' => $message,
|
||||
'type' => $type,
|
||||
'locale' => $locale,
|
||||
]), Response::MODEL_SMS_TEMPLATE);
|
||||
});
|
||||
|
||||
Http::patch('/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', false, ['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')
|
||||
->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$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);
|
||||
});
|
||||
|
||||
Http::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
|
||||
->desc('Reset custom SMS template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'deleteSmsTemplate',
|
||||
description: '/docs/references/projects/delete-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_TEMPLATE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON,
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'projects.deleteSMSTemplate',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'deleteSMSTemplate',
|
||||
description: '/docs/references/projects/delete-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_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')['sms'] ?? [], true), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
|
||||
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
|
||||
|
||||
if (is_null($template)) {
|
||||
throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION);
|
||||
}
|
||||
|
||||
unset($template['sms.' . $type . '-' . $locale]);
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'type' => $type,
|
||||
'locale' => $locale,
|
||||
'message' => $template['message']
|
||||
]), Response::MODEL_SMS_TEMPLATE);
|
||||
});
|
||||
|
||||
Http::delete('/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', false, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$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'])
|
||||
@@ -1285,3 +647,30 @@ Http::patch('/v1/projects/:projectId/auth/session-invalidation')
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
// 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')
|
||||
->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')
|
||||
->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', []);
|
||||
unset($templates['email.' . $type . '-' . $locale]);
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
@@ -2252,8 +2252,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(
|
||||
|
||||
@@ -7,9 +7,9 @@ use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete as DeleteEvent;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Network\Cors;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
@@ -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;
|
||||
@@ -897,6 +899,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 +926,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());
|
||||
}
|
||||
@@ -1014,11 +1022,11 @@ Http::init()
|
||||
->inject('request')
|
||||
->inject('console')
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->inject('certifiedDomains')
|
||||
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
|
||||
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
|
||||
$hostname = $request->getHostname();
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
|
||||
@@ -1044,7 +1052,7 @@ Http::init()
|
||||
}
|
||||
|
||||
// 4. Check/create rule (requires DB access)
|
||||
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) {
|
||||
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $publisherForCertificates, $certifiedDomains) {
|
||||
try {
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
@@ -1100,10 +1108,11 @@ Http::init()
|
||||
$dbForPlatform->createDocument('rules', $document);
|
||||
|
||||
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
|
||||
$queueForCertificates
|
||||
->setDomain($document)
|
||||
->setSkipRenewCheck(true)
|
||||
->trigger();
|
||||
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: $console,
|
||||
domain: $document,
|
||||
skipRenewCheck: true,
|
||||
));
|
||||
} catch (Duplicate $e) {
|
||||
Console::info('Certificate already exists');
|
||||
} finally {
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Context\Audit as AuditContext;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Message\Audit as AuditMessage;
|
||||
use Appwrite\Event\Message\Usage as UsageMessage;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Publisher\Audit;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Webhook;
|
||||
@@ -88,7 +90,7 @@ Http::init()
|
||||
->inject('request')
|
||||
->inject('dbForPlatform')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForAudits')
|
||||
->inject('auditContext')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('session')
|
||||
@@ -97,7 +99,7 @@ Http::init()
|
||||
->inject('team')
|
||||
->inject('apiKey')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
$route = $utopia->getRoute();
|
||||
if ($route === null) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
@@ -193,7 +195,7 @@ Http::init()
|
||||
'name' => $apiKey->getName(),
|
||||
]);
|
||||
|
||||
$queueForAudits->setUser($user);
|
||||
$auditContext->user = $user;
|
||||
}
|
||||
|
||||
// For standard keys, update last accessed time
|
||||
@@ -264,7 +266,7 @@ Http::init()
|
||||
API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION,
|
||||
default => ACTIVITY_TYPE_KEY_PROJECT,
|
||||
});
|
||||
$queueForAudits->setUser($userClone);
|
||||
$auditContext->user = $userClone;
|
||||
}
|
||||
|
||||
// Apply permission
|
||||
@@ -486,7 +488,7 @@ Http::init()
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMessaging')
|
||||
->inject('queueForAudits')
|
||||
->inject('auditContext')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForBuilds')
|
||||
@@ -503,7 +505,7 @@ Http::init()
|
||||
->inject('telemetry')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, 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) {
|
||||
->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);
|
||||
@@ -596,13 +598,12 @@ Http::init()
|
||||
->setProject($project)
|
||||
->setUser($user);
|
||||
|
||||
$queueForAudits
|
||||
->setMode($mode)
|
||||
->setUserAgent($request->getUserAgent(''))
|
||||
->setIP($request->getIP())
|
||||
->setHostname($request->getHostname())
|
||||
->setEvent($route->getLabel('audits.event', ''))
|
||||
->setProject($project);
|
||||
$auditContext->mode = $mode;
|
||||
$auditContext->userAgent = $request->getUserAgent('');
|
||||
$auditContext->ip = $request->getIP();
|
||||
$auditContext->hostname = $request->getHostname();
|
||||
$auditContext->event = $route->getLabel('audits.event', '');
|
||||
$auditContext->project = $project;
|
||||
|
||||
/* If a session exists, use the user associated with the session */
|
||||
if (! $user->isEmpty()) {
|
||||
@@ -611,7 +612,7 @@ Http::init()
|
||||
if (empty($user->getAttribute('type'))) {
|
||||
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
|
||||
}
|
||||
$queueForAudits->setUser($userClone);
|
||||
$auditContext->user = $userClone;
|
||||
}
|
||||
|
||||
/* Auto-set projects */
|
||||
@@ -790,7 +791,8 @@ Http::shutdown()
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForAudits')
|
||||
->inject('auditContext')
|
||||
->inject('publisherForAudits')
|
||||
->inject('usage')
|
||||
->inject('publisherForUsage')
|
||||
->inject('queueForDeletes')
|
||||
@@ -807,7 +809,7 @@ Http::shutdown()
|
||||
->inject('bus')
|
||||
->inject('apiKey')
|
||||
->inject('mode')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, 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) {
|
||||
->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();
|
||||
|
||||
@@ -902,7 +904,7 @@ Http::shutdown()
|
||||
if (! empty($pattern)) {
|
||||
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
if (! empty($resource) && $resource !== $pattern) {
|
||||
$queueForAudits->setResource($resource);
|
||||
$auditContext->resource = $resource;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -912,8 +914,8 @@ Http::shutdown()
|
||||
if (empty($user->getAttribute('type'))) {
|
||||
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
|
||||
}
|
||||
$queueForAudits->setUser($userClone);
|
||||
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
|
||||
$auditContext->user = $userClone;
|
||||
} elseif ($auditContext->user === null || $auditContext->user->isEmpty()) {
|
||||
/**
|
||||
* User in the request is empty, and no user was set for auditing previously.
|
||||
* This indicates:
|
||||
@@ -931,24 +933,35 @@ Http::shutdown()
|
||||
'name' => 'Guest',
|
||||
]);
|
||||
|
||||
$queueForAudits->setUser($user);
|
||||
$auditContext->user = $user;
|
||||
}
|
||||
|
||||
if (! empty($queueForAudits->getResource()) && ! $queueForAudits->getUser()->isEmpty()) {
|
||||
$auditUser = $auditContext->user;
|
||||
if (! empty($auditContext->resource) && ! \is_null($auditUser) && ! $auditUser->isEmpty()) {
|
||||
/**
|
||||
* audits.payload is switched to default true
|
||||
* in order to auto audit payload for all endpoints
|
||||
*/
|
||||
$pattern = $route->getLabel('audits.payload', true);
|
||||
if (! empty($pattern)) {
|
||||
$queueForAudits->setPayload($responsePayload);
|
||||
$pattern = $route->getLabel('audits.payload.allowList', []);
|
||||
if(empty($pattern)) {
|
||||
$auditContext->payload = $responsePayload;
|
||||
} else {
|
||||
$auditContextPayload = [];
|
||||
|
||||
foreach ($pattern as $key) {
|
||||
if (isset($responsePayload[$key])) {
|
||||
$auditContextPayload[$key] = $responsePayload[$key];
|
||||
}
|
||||
}
|
||||
|
||||
$auditContext->payload = $auditContextPayload;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
foreach ($queueForEvents->getParams() as $key => $value) {
|
||||
$queueForAudits->setParam($key, $value);
|
||||
}
|
||||
|
||||
$queueForAudits->trigger();
|
||||
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
|
||||
}
|
||||
|
||||
if (! empty($queueForDeletes->getType())) {
|
||||
@@ -972,7 +985,8 @@ Http::shutdown()
|
||||
if ($useCache) {
|
||||
$resource = $resourceType = null;
|
||||
$data = $response->getPayload();
|
||||
if (! empty($data['payload'])) {
|
||||
$statusCode = $response->getStatusCode();
|
||||
if (! empty($data['payload']) && $statusCode >= 200 && $statusCode < 300) {
|
||||
$pattern = $route->getLabel('cache.resource', null);
|
||||
if (! empty($pattern)) {
|
||||
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
|
||||
+7
-16
@@ -72,8 +72,6 @@ $swooleAdapter = new Server(
|
||||
container: $container,
|
||||
);
|
||||
|
||||
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
|
||||
|
||||
$http = $swooleAdapter->getServer();
|
||||
|
||||
/**
|
||||
@@ -415,27 +413,19 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
|
||||
$projectCollections = $collections['projects'];
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
|
||||
$sharedTablesV2 = \array_diff($sharedTables, $sharedTablesV1);
|
||||
|
||||
$documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
|
||||
$documentsSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
|
||||
$documentsSharedTablesV2 = \array_diff($documentsSharedTables, $documentsSharedTablesV1);
|
||||
|
||||
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
|
||||
$vectorSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
|
||||
$vectorSharedTablesV2 = \array_diff($vectorSharedTables, $vectorSharedTablesV1);
|
||||
|
||||
$cache = $app->getResource('cache');
|
||||
|
||||
// All shared tables V2 pools that need project metadata collections
|
||||
$sharedTablesV2All = \array_values(\array_unique(\array_filter([
|
||||
...$sharedTablesV2,
|
||||
...$documentsSharedTablesV2,
|
||||
...$vectorSharedTablesV2,
|
||||
// All shared tables pools that need project metadata collections
|
||||
$allSharedTables = \array_values(\array_unique(\array_filter([
|
||||
...$sharedTables,
|
||||
...$documentsSharedTables,
|
||||
...$vectorSharedTables,
|
||||
])));
|
||||
|
||||
foreach ($sharedTablesV2All as $hostname) {
|
||||
foreach ($allSharedTables as $hostname) {
|
||||
Span::init('database.setup');
|
||||
Span::add('database.hostname', $hostname);
|
||||
|
||||
@@ -533,6 +523,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
|
||||
}
|
||||
|
||||
$requestContainer = $swooleAdapter->getContainer();
|
||||
$requestContainer->set('container', fn () => $requestContainer);
|
||||
$requestContainer->set('request', fn () => $request);
|
||||
$requestContainer->set('response', fn () => $response);
|
||||
|
||||
|
||||
@@ -46,8 +46,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';
|
||||
|
||||
+4
-4
@@ -117,7 +117,9 @@ use Appwrite\Utopia\Response\Model\Project;
|
||||
use Appwrite\Utopia\Response\Model\Provider;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepository;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
|
||||
use Appwrite\Utopia\Response\Model\ResourceToken;
|
||||
use Appwrite\Utopia\Response\Model\Row;
|
||||
use Appwrite\Utopia\Response\Model\Rule;
|
||||
@@ -135,7 +137,6 @@ use Appwrite\Utopia\Response\Model\TemplateFramework;
|
||||
use Appwrite\Utopia\Response\Model\TemplateFunction;
|
||||
use Appwrite\Utopia\Response\Model\TemplateRuntime;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSite;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSMS;
|
||||
use Appwrite\Utopia\Response\Model\TemplateVariable;
|
||||
use Appwrite\Utopia\Response\Model\Token;
|
||||
use Appwrite\Utopia\Response\Model\Topic;
|
||||
@@ -190,8 +191,8 @@ Response::setModel(new BaseList('Site Templates List', Response::MODEL_TEMPLATE_
|
||||
Response::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
|
||||
Response::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
|
||||
Response::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
|
||||
Response::setModel(new BaseList('Framework Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtime Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME));
|
||||
Response::setModel(new ProviderRepositoryFrameworkList());
|
||||
Response::setModel(new ProviderRepositoryRuntimeList());
|
||||
Response::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
|
||||
Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIST, 'frameworks', Response::MODEL_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
|
||||
@@ -373,7 +374,6 @@ Response::setModel(new Headers());
|
||||
Response::setModel(new Specification());
|
||||
Response::setModel(new Rule());
|
||||
Response::setModel(new Schedule());
|
||||
Response::setModel(new TemplateSMS());
|
||||
Response::setModel(new TemplateEmail());
|
||||
Response::setModel(new ConsoleVariables());
|
||||
Response::setModel(new MFAChallenge());
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Audit as AuditPublisher;
|
||||
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
|
||||
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
|
||||
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
|
||||
use Appwrite\Event\Publisher\Screenshot as ScreenshotPublisher;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
@@ -81,6 +84,18 @@ $container->set('publisherMessaging', function (Publisher $publisher) {
|
||||
$container->set('publisherWebhooks', function (Publisher $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$container->set('publisherForAudits', fn (Publisher $publisher) => new AuditPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForScreenshots', fn (Publisher $publisher) => new ScreenshotPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
|
||||
@@ -4,9 +4,8 @@ use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Audit as AuditEvent;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Context\Audit as AuditContext;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
@@ -14,7 +13,6 @@ use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
@@ -128,9 +126,6 @@ return function (Container $container): void {
|
||||
$container->set('queueForBuilds', function (Publisher $publisher) {
|
||||
return new Build($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('queueForScreenshots', function (Publisher $publisher) {
|
||||
return new Screenshot($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('queueForDatabase', function (Publisher $publisher) {
|
||||
return new EventDatabase($publisher);
|
||||
}, ['publisher']);
|
||||
@@ -149,18 +144,13 @@ return function (Container $container): void {
|
||||
$container->set('usage', function () {
|
||||
return new UsageContext();
|
||||
}, []);
|
||||
$container->set('queueForAudits', function (Publisher $publisher) {
|
||||
return new AuditEvent($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('auditContext', fn () => new AuditContext(), []);
|
||||
$container->set('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('eventProcessor', function () {
|
||||
return new EventProcessor();
|
||||
}, []);
|
||||
$container->set('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
@@ -10,7 +8,6 @@ use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
@@ -311,10 +308,6 @@ return function (Container $container): void {
|
||||
return new Build($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForScreenshots', function (Publisher $publisher) {
|
||||
return new Screenshot($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
@@ -323,10 +316,6 @@ return function (Container $container): void {
|
||||
return new Event($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForAudits', function (Publisher $publisher) {
|
||||
return new Audit($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForWebhooks', function (Publisher $publisher) {
|
||||
return new Webhook($publisher);
|
||||
}, ['publisher']);
|
||||
@@ -339,10 +328,6 @@ return function (Container $container): void {
|
||||
return new Realtime();
|
||||
}, []);
|
||||
|
||||
$container->set('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
@@ -398,6 +398,11 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$register->set('telemetry.connectionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.open_connections'));
|
||||
$register->set('telemetry.connectionCreatedCounter', fn () => $telemetry->createCounter('realtime.server.connection.created'));
|
||||
$register->set('telemetry.messageSentCounter', fn () => $telemetry->createCounter('realtime.server.message.sent'));
|
||||
$register->set('telemetry.deliveryDelayHistogram', fn () => $telemetry->createHistogram(
|
||||
name: 'realtime.server.delivery_delay',
|
||||
unit: 'ms',
|
||||
advisory: ['ExplicitBucketBoundaries' => [100, 250, 500, 750, 1000, 1500, 2000, 3000, 5000, 7500, 10000, 15000, 30000]],
|
||||
));
|
||||
|
||||
$attempts = 0;
|
||||
$start = time();
|
||||
@@ -592,6 +597,20 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
if ($total > 0) {
|
||||
$register->get('telemetry.messageSentCounter')->add($total);
|
||||
$stats->incr($event['project'], 'messages', $total);
|
||||
$updatedAt = $event['data']['payload']['$updatedAt'] ?? null;
|
||||
if (\is_string($updatedAt)) {
|
||||
try {
|
||||
$updatedAtDate = new \DateTimeImmutable($updatedAt);
|
||||
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
$updatedAtTimestampMs = (float) $updatedAtDate->format('U.u') * 1000;
|
||||
$nowTimestampMs = (float) $now->format('U.u') * 1000;
|
||||
$delayMs = (int) \max(0, $nowTimestampMs - $updatedAtTimestampMs);
|
||||
|
||||
$register->get('telemetry.deliveryDelayHistogram')->record($delayMs);
|
||||
} catch (\Throwable) {
|
||||
// Ignore invalid timestamp payloads.
|
||||
}
|
||||
}
|
||||
|
||||
$projectId = $event['project'] ?? null;
|
||||
|
||||
|
||||
@@ -120,7 +120,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_SMTP_HOST
|
||||
- _APP_SMTP_PORT
|
||||
- _APP_SMTP_SECURE
|
||||
@@ -256,7 +255,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
@@ -287,7 +285,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-webhooks:
|
||||
@@ -315,7 +312,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -356,7 +352,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
@@ -416,7 +411,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-builds:
|
||||
@@ -453,7 +447,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
@@ -529,7 +522,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-executions:
|
||||
@@ -592,7 +584,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_SITES_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
@@ -630,7 +621,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -673,7 +663,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_SMS_FROM
|
||||
- _APP_SMS_PROVIDER
|
||||
@@ -734,7 +723,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
@@ -773,7 +761,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
@@ -806,7 +793,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -839,7 +825,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -871,7 +856,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -907,7 +891,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-executions:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -936,7 +919,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-messages:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -965,7 +947,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
<?php if ($enableAssistant): ?>
|
||||
appwrite-assistant:
|
||||
@@ -1068,13 +1049,12 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-mongodb:/data/db
|
||||
- appwrite-mongodb-keyfile:/data/keyfile
|
||||
ports:
|
||||
- "27017:27017"
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
|
||||
@@ -1205,7 +1185,6 @@ volumes:
|
||||
<?php elseif ($dbService === 'mongodb'): ?>
|
||||
appwrite-mongodb:
|
||||
appwrite-mongodb-keyfile:
|
||||
appwrite-mongodb-config:
|
||||
<?php endif; ?>
|
||||
appwrite-redis:
|
||||
appwrite-cache:
|
||||
|
||||
+2
-2
@@ -74,7 +74,7 @@
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.22.*",
|
||||
"utopia-php/migration": "1.9.*",
|
||||
"utopia-php/platform": "0.12.*",
|
||||
"utopia-php/platform": "0.13.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
@@ -92,7 +92,7 @@
|
||||
"chillerlan/php-qrcode": "4.3.*",
|
||||
"adhocore/jwt": "1.1.*",
|
||||
"spomky-labs/otphp": "11.*",
|
||||
"webonyx/graphql-php": "14.11.*",
|
||||
"webonyx/graphql-php": "15.31.*",
|
||||
"league/csv": "9.14.*",
|
||||
"enshrined/svg-sanitize": "0.22.*"
|
||||
},
|
||||
|
||||
Generated
+118
-103
@@ -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": "4fb974e9843f6104e40396e7cad4a833",
|
||||
"content-hash": "c5ae97637fd0ec0a950044d1c33677ea",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -2887,7 +2887,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
@@ -2948,7 +2948,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2972,7 +2972,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php82",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php82.git",
|
||||
@@ -3028,7 +3028,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.36.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3052,7 +3052,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php83",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||
@@ -3108,7 +3108,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.36.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3132,7 +3132,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php85",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php85.git",
|
||||
@@ -3188,7 +3188,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-php85/tree/v1.36.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.20",
|
||||
"version": "5.3.21",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24"
|
||||
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
|
||||
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
|
||||
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
|
||||
"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.20"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.21"
|
||||
},
|
||||
"time": "2026-04-10T08:27:41+00:00"
|
||||
"time": "2026-04-10T12:38:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4271,16 +4271,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/http",
|
||||
"version": "0.34.20",
|
||||
"version": "0.34.21",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "d6b360d555022d16c16d40be51f86180364819f8"
|
||||
"reference": "49a6bd3ea0d2966aa19cf707255d442675288a24"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/d6b360d555022d16c16d40be51f86180364819f8",
|
||||
"reference": "d6b360d555022d16c16d40be51f86180364819f8",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/49a6bd3ea0d2966aa19cf707255d442675288a24",
|
||||
"reference": "49a6bd3ea0d2966aa19cf707255d442675288a24",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4319,22 +4319,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.20"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.21"
|
||||
},
|
||||
"time": "2026-04-12T14:25:22+00:00"
|
||||
"time": "2026-04-19T19:44:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/image.git",
|
||||
"reference": "ce788ff0121a79286fdbe3ef3eba566de646df65"
|
||||
"reference": "85ab7027873e11bc901110d8f7830252247ba724"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/image/zipball/ce788ff0121a79286fdbe3ef3eba566de646df65",
|
||||
"reference": "ce788ff0121a79286fdbe3ef3eba566de646df65",
|
||||
"url": "https://api.github.com/repos/utopia-php/image/zipball/85ab7027873e11bc901110d8f7830252247ba724",
|
||||
"reference": "85ab7027873e11bc901110d8f7830252247ba724",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4343,10 +4343,12 @@
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
"phpstan/phpstan": "^1.10.0",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"vimeo/psalm": "4.13.1"
|
||||
"laravel/pint": "1.24.*",
|
||||
"phpstan/phpstan": "2.1.*",
|
||||
"phpunit/phpunit": "10.5.*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "Imagick extension is required for Imagick adapter"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -4368,9 +4370,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/image/issues",
|
||||
"source": "https://github.com/utopia-php/image/tree/0.8.4"
|
||||
"source": "https://github.com/utopia-php/image/tree/0.8.6"
|
||||
},
|
||||
"time": "2025-06-03T08:32:20+00:00"
|
||||
"time": "2026-04-19T12:52:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/locale",
|
||||
@@ -4642,16 +4644,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/platform",
|
||||
"version": "0.12.1",
|
||||
"version": "0.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/platform.git",
|
||||
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc"
|
||||
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
|
||||
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/d23af5349a7ea9ee11f9920a13626226f985522e",
|
||||
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4687,9 +4689,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/platform/issues",
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.12.1"
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.13.0"
|
||||
},
|
||||
"time": "2026-04-08T04:11:31+00:00"
|
||||
"time": "2026-04-17T09:57:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/pools",
|
||||
@@ -5381,38 +5383,48 @@
|
||||
},
|
||||
{
|
||||
"name": "webonyx/graphql-php",
|
||||
"version": "v14.11.10",
|
||||
"version": "v15.31.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/webonyx/graphql-php.git",
|
||||
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19"
|
||||
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9c2fdebc6aa01d831bc2969da00e8588cffef19",
|
||||
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19",
|
||||
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/089c4ef7e112df85788cfe06596278a8f99f4aa9",
|
||||
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8"
|
||||
"php": "^7.4 || ^8"
|
||||
},
|
||||
"require-dev": {
|
||||
"amphp/amp": "^2.3",
|
||||
"doctrine/coding-standard": "^6.0",
|
||||
"nyholm/psr7": "^1.2",
|
||||
"amphp/amp": "^2.6",
|
||||
"amphp/http-server": "^2.1",
|
||||
"dms/phpunit-arraysubset-asserts": "dev-master",
|
||||
"ergebnis/composer-normalize": "^2.28",
|
||||
"friendsofphp/php-cs-fixer": "3.94.2",
|
||||
"mll-lab/php-cs-fixer-config": "5.13.0",
|
||||
"nyholm/psr7": "^1.5",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "0.12.82",
|
||||
"phpstan/phpstan-phpunit": "0.12.18",
|
||||
"phpstan/phpstan-strict-rules": "0.12.9",
|
||||
"phpunit/phpunit": "^7.2 || ^8.5",
|
||||
"psr/http-message": "^1.0",
|
||||
"react/promise": "2.*",
|
||||
"simpod/php-coveralls-mirror": "^3.0"
|
||||
"phpstan/extension-installer": "^1.1",
|
||||
"phpstan/phpstan": "2.1.46",
|
||||
"phpstan/phpstan-phpunit": "2.0.16",
|
||||
"phpstan/phpstan-strict-rules": "2.0.10",
|
||||
"phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
|
||||
"psr/http-message": "^1 || ^2",
|
||||
"react/http": "^1.6",
|
||||
"react/promise": "^2.0 || ^3.0",
|
||||
"rector/rector": "^2.0",
|
||||
"symfony/polyfill-php81": "^1.23",
|
||||
"symfony/var-exporter": "^5 || ^6 || ^7 || ^8",
|
||||
"thecodingmachine/safe": "^1.3 || ^2 || ^3",
|
||||
"ticketswap/phpstan-error-formatter": "1.3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
|
||||
"psr/http-message": "To use standard GraphQL server",
|
||||
"react/promise": "To leverage async resolving on React PHP platform"
|
||||
},
|
||||
@@ -5434,30 +5446,34 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/webonyx/graphql-php/issues",
|
||||
"source": "https://github.com/webonyx/graphql-php/tree/v14.11.10"
|
||||
"source": "https://github.com/webonyx/graphql-php/tree/v15.31.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/spawnia",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/webonyx-graphql-php",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-05T14:23:37+00:00"
|
||||
"time": "2026-04-11T18:06:15+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.17.11",
|
||||
"version": "1.20",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250"
|
||||
"reference": "525f0630520c95100fcdfb63c9dac859c1d02588"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/c714ee52659ef5968b3372ff4da0e407140a6250",
|
||||
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/525f0630520c95100fcdfb63c9dac859c1d02588",
|
||||
"reference": "525f0630520c95100fcdfb63c9dac859c1d02588",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5493,9 +5509,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.17.11"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.20"
|
||||
},
|
||||
"time": "2026-04-11T02:42:32+00:00"
|
||||
"time": "2026-04-20T05:45:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
@@ -6204,11 +6220,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.46",
|
||||
"version": "2.1.50",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d452086fb4cf648c6b2d8cf3b639351f79e4f3e2",
|
||||
"reference": "d452086fb4cf648c6b2d8cf3b639351f79e4f3e2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6253,20 +6269,20 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-01T09:25:14+00:00"
|
||||
"time": "2026-04-17T13:10:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "12.5.3",
|
||||
"version": "12.5.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||
"reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d"
|
||||
"reference": "876099a072646c7745f673d7aeab5382c4439691"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d",
|
||||
"reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691",
|
||||
"reference": "876099a072646c7745f673d7aeab5382c4439691",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6275,7 +6291,6 @@
|
||||
"ext-xmlwriter": "*",
|
||||
"nikic/php-parser": "^5.7.0",
|
||||
"php": ">=8.3",
|
||||
"phpunit/php-file-iterator": "^6.0",
|
||||
"phpunit/php-text-template": "^5.0",
|
||||
"sebastian/complexity": "^5.0",
|
||||
"sebastian/environment": "^8.0.3",
|
||||
@@ -6322,7 +6337,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3"
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6342,7 +6357,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-06T06:01:44+00:00"
|
||||
"time": "2026-04-15T08:23:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
@@ -6603,16 +6618,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "12.5.17",
|
||||
"version": "12.5.23",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b"
|
||||
"reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/85b62adab1a340982df64e66daa4a4435eb5723b",
|
||||
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969",
|
||||
"reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6626,15 +6641,15 @@
|
||||
"phar-io/manifest": "^2.0.4",
|
||||
"phar-io/version": "^3.2.1",
|
||||
"php": ">=8.3",
|
||||
"phpunit/php-code-coverage": "^12.5.3",
|
||||
"phpunit/php-code-coverage": "^12.5.6",
|
||||
"phpunit/php-file-iterator": "^6.0.1",
|
||||
"phpunit/php-invoker": "^6.0.0",
|
||||
"phpunit/php-text-template": "^5.0.0",
|
||||
"phpunit/php-timer": "^8.0.0",
|
||||
"sebastian/cli-parser": "^4.2.0",
|
||||
"sebastian/comparator": "^7.1.4",
|
||||
"sebastian/comparator": "^7.1.6",
|
||||
"sebastian/diff": "^7.0.0",
|
||||
"sebastian/environment": "^8.0.4",
|
||||
"sebastian/environment": "^8.1.0",
|
||||
"sebastian/exporter": "^7.0.2",
|
||||
"sebastian/global-state": "^8.0.2",
|
||||
"sebastian/object-enumerator": "^7.0.0",
|
||||
@@ -6681,7 +6696,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.17"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.23"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6689,7 +6704,7 @@
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-08T03:04:19+00:00"
|
||||
"time": "2026-04-18T06:12:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
@@ -6762,16 +6777,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/comparator",
|
||||
"version": "7.1.5",
|
||||
"version": "7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/comparator.git",
|
||||
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63"
|
||||
"reference": "c769009dee98f494e0edc3fd4f4087501688f11e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c284f55811f43d555e51e8e5c166ac40d3e33c63",
|
||||
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c769009dee98f494e0edc3fd4f4087501688f11e",
|
||||
"reference": "c769009dee98f494e0edc3fd4f4087501688f11e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6830,7 +6845,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/comparator/issues",
|
||||
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.5"
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6850,7 +6865,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-08T04:43:00+00:00"
|
||||
"time": "2026-04-14T08:23:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/complexity",
|
||||
@@ -6979,16 +6994,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/environment",
|
||||
"version": "8.0.4",
|
||||
"version": "8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/environment.git",
|
||||
"reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11"
|
||||
"reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11",
|
||||
"reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b121608b28a13f721e76ffbbd386d08eff58f3f6",
|
||||
"reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7003,7 +7018,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "8.0-dev"
|
||||
"dev-main": "8.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -7031,7 +7046,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/environment/issues",
|
||||
"security": "https://github.com/sebastianbergmann/environment/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/environment/tree/8.0.4"
|
||||
"source": "https://github.com/sebastianbergmann/environment/tree/8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7051,7 +7066,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-15T07:05:40+00:00"
|
||||
"time": "2026-04-15T12:13:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/exporter",
|
||||
@@ -7764,7 +7779,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
@@ -7823,7 +7838,7 @@
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7847,7 +7862,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
@@ -7905,7 +7920,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7929,7 +7944,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
@@ -7990,7 +8005,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8014,7 +8029,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php81",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php81.git",
|
||||
@@ -8070,7 +8085,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.36.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8449,5 +8464,5 @@
|
||||
"platform-dev": {
|
||||
"ext-fileinfo": "*"
|
||||
},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
+2
-3
@@ -242,7 +242,6 @@ services:
|
||||
- _APP_EXPERIMENT_LOGGING_PROVIDER
|
||||
- _APP_EXPERIMENT_LOGGING_CONFIG
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
- _APP_DATABASE_SHARED_TABLES_V1
|
||||
- _APP_DATABASE_SHARED_NAMESPACE
|
||||
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
|
||||
- _APP_CUSTOM_DOMAIN_DENY_LIST
|
||||
@@ -254,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
|
||||
@@ -462,7 +461,6 @@ services:
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
- _APP_DATABASE_SHARED_TABLES_V1
|
||||
- _APP_EMAIL_CERTIFICATES
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE
|
||||
@@ -1288,6 +1286,7 @@ services:
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
restart: on-failure:3
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Reset a custom SMS template to its default value. This endpoint removes any custom message and restores the template to its original state.
|
||||
@@ -1 +0,0 @@
|
||||
Get a custom SMS template for the specified locale and type returning it's contents.
|
||||
@@ -1 +0,0 @@
|
||||
Update a custom SMS template for the specified locale and type. Use this endpoint to modify the content of your SMS templates.
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -7,6 +7,8 @@ use Appwrite\Event\Message\Execution as ExecutionMessage;
|
||||
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
|
||||
use Utopia\Bus\Listener;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Log extends Listener
|
||||
{
|
||||
@@ -30,9 +32,27 @@ class Log extends Listener
|
||||
|
||||
public function handle(ExecutionCompleted $event, ExecutionPublisher $publisherForExecutions): void
|
||||
{
|
||||
$project = new Document($event->project);
|
||||
$execution = new Document($event->execution);
|
||||
if ($execution->getAttribute('resourceType', '') === 'functions') {
|
||||
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
|
||||
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
|
||||
$resourceId = $execution->getAttribute('resourceId', '');
|
||||
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
|
||||
Span::init('execution.trace.v1_executions_enqueue');
|
||||
Span::add('datetime', gmdate('c'));
|
||||
Span::add('projectId', $project->getId());
|
||||
Span::add('functionId', $resourceId);
|
||||
Span::add('executionId', $execution->getId());
|
||||
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
|
||||
Span::add('status', $execution->getAttribute('status', ''));
|
||||
Span::current()?->finish();
|
||||
}
|
||||
}
|
||||
|
||||
$publisherForExecutions->enqueue(new ExecutionMessage(
|
||||
project: new Document($event->project),
|
||||
execution: new Document($event->execution),
|
||||
project: $project,
|
||||
execution: $execution,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,9 @@ class Mails extends Listener
|
||||
throw new \Exception('Invalid template path');
|
||||
}
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])["email.sessionAlert-$event->locale"] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])["email.sessionAlert-" . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.sessionAlert-' . $locale->fallback] ?? [];
|
||||
$isBranded = $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE;
|
||||
|
||||
$subject = $customTemplate['subject'] ?? $locale->getText('emails.sessionAlert.subject');
|
||||
@@ -131,7 +133,8 @@ class Mails extends Listener
|
||||
->setSmtpUsername($smtp['username'] ?? '')
|
||||
->setSmtpPassword($smtp['password'] ?? '')
|
||||
->setSmtpSecure($smtp['secure'] ?? '')
|
||||
->setSmtpReplyTo($customTemplate['replyTo'] ?? $smtp['replyTo'] ?? '')
|
||||
->setSmtpReplyToEmail($customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '') // Includes backwards compatibility
|
||||
->setSmtpReplyToName($customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '')
|
||||
->setSmtpSenderEmail($customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM))
|
||||
->setSmtpSenderName($customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Context;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class Audit
|
||||
{
|
||||
public function __construct(
|
||||
public ?Document $project = null,
|
||||
public ?Document $user = null,
|
||||
public string $mode = '',
|
||||
public string $userAgent = '',
|
||||
public string $ip = '',
|
||||
public string $hostname = '',
|
||||
public string $event = '',
|
||||
public string $resource = '',
|
||||
public array $payload = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->project === null
|
||||
&& $this->user === null
|
||||
&& $this->mode === ''
|
||||
&& $this->userAgent === ''
|
||||
&& $this->ip === ''
|
||||
&& $this->hostname === ''
|
||||
&& $this->event === ''
|
||||
&& $this->resource === ''
|
||||
&& $this->payload === [];
|
||||
}
|
||||
}
|
||||
@@ -637,9 +637,11 @@ class Event
|
||||
*/
|
||||
$eventValues = \array_values($events);
|
||||
|
||||
/**
|
||||
* Return a combined list of table, collection events and if tablesdb present then include all for backward compatibility
|
||||
*/
|
||||
$databaseType = $database?->getAttribute('type', 'legacy');
|
||||
if ($database !== null && !\in_array($databaseType, ['legacy', 'tablesdb'], true)) {
|
||||
return $eventValues;
|
||||
}
|
||||
|
||||
return Event::mirrorCollectionEvents($pattern, $eventValues[0], $eventValues);
|
||||
}
|
||||
|
||||
@@ -662,21 +664,30 @@ class Event
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `table` events for `collection` events.
|
||||
* Adds table/collection counterpart events for backward compatibility.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* `databases.*.collections.*.documents.*.update` →\
|
||||
* `[databases.*.collections.*.documents.*.update, databases.*.tables.*.rows.*.update]`
|
||||
*
|
||||
* `databases.*.tables.*.rows.*.update` →\
|
||||
* `[databases.*.tables.*.rows.*.update, databases.*.collections.*.documents.*.update]`
|
||||
*/
|
||||
private static function mirrorCollectionEvents(string $pattern, string $firstEvent, array $events): array
|
||||
{
|
||||
$tableEventMap = [
|
||||
$collectionsToTablesMap = [
|
||||
'documents' => 'rows',
|
||||
'collections' => 'tables',
|
||||
'attributes' => 'columns',
|
||||
];
|
||||
|
||||
$tablesToCollectionsMap = [
|
||||
'rows' => 'documents',
|
||||
'tables' => 'collections',
|
||||
'columns' => 'attributes',
|
||||
];
|
||||
|
||||
$databasesEventMap = [
|
||||
'tablesdb' => 'databases',
|
||||
'tables' => 'collections',
|
||||
@@ -687,7 +698,10 @@ class Event
|
||||
if (
|
||||
(
|
||||
str_contains($pattern, 'databases.') &&
|
||||
str_contains($firstEvent, 'collections')
|
||||
(
|
||||
str_contains($firstEvent, 'collections') ||
|
||||
str_contains($firstEvent, 'tables')
|
||||
)
|
||||
) ||
|
||||
(
|
||||
str_contains($firstEvent, 'tablesdb.')
|
||||
@@ -698,25 +712,16 @@ class Event
|
||||
$pairedEvents[] = $event;
|
||||
// tablesdb needs databases event with tables and collections
|
||||
if (str_contains($event, 'tablesdb')) {
|
||||
$databasesSideEvent = str_replace(
|
||||
array_keys($databasesEventMap),
|
||||
array_values($databasesEventMap),
|
||||
$event
|
||||
);
|
||||
$databasesSideEvent = self::replaceEventSegments($event, $databasesEventMap);
|
||||
$pairedEvents[] = $databasesSideEvent;
|
||||
$tableSideEvent = str_replace(
|
||||
array_keys($tableEventMap),
|
||||
array_values($tableEventMap),
|
||||
$databasesSideEvent
|
||||
);
|
||||
$tableSideEvent = self::replaceEventSegments($databasesSideEvent, $collectionsToTablesMap);
|
||||
$pairedEvents[] = $tableSideEvent;
|
||||
} elseif (str_contains($event, 'collections')) {
|
||||
$tableSideEvent = str_replace(
|
||||
array_keys($tableEventMap),
|
||||
array_values($tableEventMap),
|
||||
$event
|
||||
);
|
||||
$tableSideEvent = self::replaceEventSegments($event, $collectionsToTablesMap);
|
||||
$pairedEvents[] = $tableSideEvent;
|
||||
} elseif (str_contains($event, 'tables')) {
|
||||
$collectionSideEvent = self::replaceEventSegments($event, $tablesToCollectionsMap);
|
||||
$pairedEvents[] = $collectionSideEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,6 +733,20 @@ class Event
|
||||
return array_values(array_unique($events));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace only exact event path segments, never partial substrings.
|
||||
*/
|
||||
private static function replaceEventSegments(string $event, array $map): string
|
||||
{
|
||||
$parts = \explode('.', $event);
|
||||
$parts = \array_map(
|
||||
fn (string $part) => $map[$part] ?? $part,
|
||||
$parts
|
||||
);
|
||||
|
||||
return \implode('.', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps event terminology based on database type
|
||||
*/
|
||||
|
||||
@@ -53,4 +53,23 @@ class Execution extends Event
|
||||
'execution' => $this->execution,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim payload for the execution event.
|
||||
* Only the project ID is needed — the worker DI fetches the full project from the platform database.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function trimPayload(): array
|
||||
{
|
||||
$trimmed = [];
|
||||
|
||||
if ($this->project) {
|
||||
$trimmed['project'] = new Document([
|
||||
'$id' => $this->project->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,14 +251,26 @@ class Mail extends Event
|
||||
}
|
||||
|
||||
/**
|
||||
* Set SMTP reply to
|
||||
* Set SMTP reply-to email
|
||||
*
|
||||
* @param string $replyTo
|
||||
* @param string $email
|
||||
* @return self
|
||||
*/
|
||||
public function setSmtpReplyTo(string $replyTo): self
|
||||
public function setSmtpReplyToEmail(string $email): self
|
||||
{
|
||||
$this->smtp['replyTo'] = $replyTo;
|
||||
$this->smtp['replyToEmail'] = $email;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set SMTP reply-to name
|
||||
*
|
||||
* @param string $name
|
||||
* @return self
|
||||
*/
|
||||
public function setSmtpReplyToName(string $name): self
|
||||
{
|
||||
$this->smtp['replyToName'] = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -333,13 +345,23 @@ class Mail extends Event
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SMTP reply to
|
||||
* Get SMTP reply-to email
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSmtpReplyTo(): string
|
||||
public function getSmtpReplyToEmail(): string
|
||||
{
|
||||
return $this->smtp['replyTo'] ?? '';
|
||||
return $this->smtp['replyToEmail'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SMTP reply-to name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSmtpReplyToName(): string
|
||||
{
|
||||
return $this->smtp['replyToName'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Message;
|
||||
|
||||
use Appwrite\Event\Context\Audit as AuditContext;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class Audit extends Base
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $event,
|
||||
public readonly array $payload,
|
||||
public readonly Document $project = new Document(),
|
||||
public readonly Document $user = new Document(),
|
||||
public readonly string $resource = '',
|
||||
public readonly string $mode = '',
|
||||
public readonly string $ip = '',
|
||||
public readonly string $userAgent = '',
|
||||
public readonly string $hostname = '',
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'project' => [
|
||||
'$id' => $this->project->getId(),
|
||||
'$sequence' => $this->project->getSequence(),
|
||||
'database' => $this->project->getAttribute('database', ''),
|
||||
],
|
||||
'user' => $this->user->getArrayCopy(),
|
||||
'payload' => $this->payload,
|
||||
'resource' => $this->resource,
|
||||
'mode' => $this->mode,
|
||||
'ip' => $this->ip,
|
||||
'userAgent' => $this->userAgent,
|
||||
'event' => $this->event,
|
||||
'hostname' => $this->hostname,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
event: $data['event'] ?? '',
|
||||
payload: $data['payload'] ?? [],
|
||||
project: new Document($data['project'] ?? []),
|
||||
user: new Document($data['user'] ?? []),
|
||||
resource: $data['resource'] ?? '',
|
||||
mode: $data['mode'] ?? '',
|
||||
ip: $data['ip'] ?? '',
|
||||
userAgent: $data['userAgent'] ?? '',
|
||||
hostname: $data['hostname'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromContext(AuditContext $context): static
|
||||
{
|
||||
return new self(
|
||||
event: $context->event,
|
||||
payload: $context->payload,
|
||||
project: $context->project ?? new Document(),
|
||||
user: $context->user ?? new Document(),
|
||||
resource: $context->resource,
|
||||
mode: $context->mode,
|
||||
ip: $context->ip,
|
||||
userAgent: $context->userAgent,
|
||||
hostname: $context->hostname,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class Certificate extends Base
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Document $project,
|
||||
public readonly Document $domain,
|
||||
public readonly bool $skipRenewCheck = false,
|
||||
public readonly ?string $validationDomain = null,
|
||||
public readonly string $action = \Appwrite\Event\Certificate::ACTION_GENERATION,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'project' => [
|
||||
'$id' => $this->project->getId(),
|
||||
'$sequence' => $this->project->getSequence(),
|
||||
'database' => $this->project->getAttribute('database', ''),
|
||||
],
|
||||
'domain' => $this->domain->getArrayCopy(),
|
||||
'skipRenewCheck' => $this->skipRenewCheck,
|
||||
'validationDomain' => $this->validationDomain,
|
||||
'action' => $this->action,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
project: new Document($data['project'] ?? []),
|
||||
domain: new Document($data['domain'] ?? []),
|
||||
skipRenewCheck: $data['skipRenewCheck'] ?? false,
|
||||
validationDomain: $data['validationDomain'] ?? null,
|
||||
action: $data['action'] ?? \Appwrite\Event\Certificate::ACTION_GENERATION,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class Screenshot extends Base
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Document $project,
|
||||
public readonly string $deploymentId,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'project' => [
|
||||
'$id' => $this->project->getId(),
|
||||
'$sequence' => $this->project->getSequence(),
|
||||
'database' => $this->project->getAttribute('database', ''),
|
||||
],
|
||||
'deploymentId' => $this->deploymentId,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
project: new Document($data['project'] ?? []),
|
||||
deploymentId: $data['deploymentId'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Publisher;
|
||||
|
||||
use Appwrite\Event\Message\Audit as AuditMessage;
|
||||
use Utopia\Console;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
|
||||
readonly class Audit extends Base
|
||||
{
|
||||
public function __construct(
|
||||
Publisher $publisher,
|
||||
protected Queue $queue
|
||||
) {
|
||||
parent::__construct($publisher);
|
||||
}
|
||||
|
||||
public function enqueue(AuditMessage $message): string|bool
|
||||
{
|
||||
// Audit delivery is best-effort and should never fail the request lifecycle.
|
||||
try {
|
||||
return $this->publish($this->queue, $message);
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('[Audit] Failed to publish audit message: ' . $th->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSize(bool $failed = false): int
|
||||
{
|
||||
return $this->getQueueSize($this->queue, $failed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Publisher;
|
||||
|
||||
use Appwrite\Event\Message\Certificate as CertificateMessage;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
|
||||
readonly class Certificate extends Base
|
||||
{
|
||||
public function __construct(
|
||||
Publisher $publisher,
|
||||
protected Queue $queue
|
||||
) {
|
||||
parent::__construct($publisher);
|
||||
}
|
||||
|
||||
public function enqueue(CertificateMessage $message): string|bool
|
||||
{
|
||||
return $this->publish($this->queue, $message);
|
||||
}
|
||||
|
||||
public function getSize(bool $failed = false): int
|
||||
{
|
||||
return $this->getQueueSize($this->queue, $failed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Publisher;
|
||||
|
||||
use Appwrite\Event\Message\Screenshot as ScreenshotMessage;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
|
||||
readonly class Screenshot extends Base
|
||||
{
|
||||
public function __construct(
|
||||
Publisher $publisher,
|
||||
protected Queue $queue
|
||||
) {
|
||||
parent::__construct($publisher);
|
||||
}
|
||||
|
||||
public function enqueue(ScreenshotMessage $message): string|bool
|
||||
{
|
||||
return $this->publish($this->queue, $message);
|
||||
}
|
||||
|
||||
public function getSize(bool $failed = false): int
|
||||
{
|
||||
return $this->getQueueSize($this->queue, $failed);
|
||||
}
|
||||
}
|
||||
@@ -81,8 +81,8 @@ abstract class Adapter implements PromiseAdapter
|
||||
/**
|
||||
* Create a new promise that resolves when all passed in promises resolve.
|
||||
*
|
||||
* @param array $promisesOrValues
|
||||
* @param iterable $promisesOrValues
|
||||
* @return GQLPromise
|
||||
*/
|
||||
abstract public function all(array $promisesOrValues): GQLPromise;
|
||||
abstract public function all(iterable $promisesOrValues): GQLPromise;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,12 @@ class Swoole extends Adapter
|
||||
return new GQLPromise($promise, $this);
|
||||
}
|
||||
|
||||
public function all(array $promisesOrValues): GQLPromise
|
||||
public function all(iterable $promisesOrValues): GQLPromise
|
||||
{
|
||||
if ($promisesOrValues instanceof \Traversable) {
|
||||
$promisesOrValues = \iterator_to_array($promisesOrValues);
|
||||
}
|
||||
|
||||
return new GQLPromise(SwoolePromise::all($promisesOrValues), $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
|
||||
use Swoole\Coroutine;
|
||||
use Swoole\Coroutine\Channel;
|
||||
|
||||
final class ResolverLock
|
||||
{
|
||||
public Channel $channel;
|
||||
public ?int $owner = null;
|
||||
public int $depth = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->channel = new Channel(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire the lock. Re-entering from the same coroutine only
|
||||
* increments depth to avoid self-deadlock.
|
||||
*/
|
||||
public function acquire(): void
|
||||
{
|
||||
$cid = Coroutine::getCid();
|
||||
|
||||
if ($this->owner === $cid) {
|
||||
$this->depth++;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->channel->push(true);
|
||||
$this->owner = $cid;
|
||||
$this->depth = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the lock.
|
||||
*/
|
||||
public function release(): void
|
||||
{
|
||||
if ($this->owner !== Coroutine::getCid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->depth--;
|
||||
|
||||
if ($this->depth > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->owner = null;
|
||||
$this->channel->pop();
|
||||
}
|
||||
}
|
||||
+237
-108
@@ -6,6 +6,7 @@ use Appwrite\GraphQL\Exception as GQLException;
|
||||
use Appwrite\Promises\Swoole;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\Http\Exception;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Http\Route;
|
||||
@@ -13,6 +14,75 @@ use Utopia\System\System;
|
||||
|
||||
class Resolvers
|
||||
{
|
||||
/**
|
||||
* Request-scoped locks keyed by the per-request GraphQL Http instance.
|
||||
*
|
||||
* @var array<string, ResolverLock>
|
||||
*/
|
||||
private static array $locks = [];
|
||||
|
||||
/**
|
||||
* Preserve response side effects that callers depend on, such as session
|
||||
* cookies created by account auth routes.
|
||||
*/
|
||||
private static function mergeResponseSideEffects(Response $from, Response $to): void
|
||||
{
|
||||
foreach ($from->getCookies() as $cookie) {
|
||||
$to->removeCookie($cookie['name']);
|
||||
$to->addCookie(
|
||||
$cookie['name'],
|
||||
$cookie['value'],
|
||||
$cookie['expire'],
|
||||
$cookie['path'],
|
||||
$cookie['domain'],
|
||||
$cookie['secure'],
|
||||
$cookie['httponly'],
|
||||
$cookie['samesite']
|
||||
);
|
||||
}
|
||||
|
||||
$headers = $from->getHeaders();
|
||||
$fallbackCookies = $headers['X-Fallback-Cookies'] ?? null;
|
||||
if ($fallbackCookies === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$to->removeHeader('X-Fallback-Cookies');
|
||||
foreach ((array) $fallbackCookies as $value) {
|
||||
$to->addHeader('X-Fallback-Cookies', $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current request container.
|
||||
*/
|
||||
private static function getResolverContainer(Http $utopia): Container
|
||||
{
|
||||
$container = $utopia->getResource('container');
|
||||
|
||||
if ($container instanceof Container || (\is_object($container) && \method_exists($container, 'get') && \method_exists($container, 'set'))) {
|
||||
/** @var Container $container */
|
||||
return $container;
|
||||
}
|
||||
|
||||
/** @var callable(): Container $container */
|
||||
return $container();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request-scoped lock shared by GraphQL resolver coroutines
|
||||
* for the current HTTP request.
|
||||
*/
|
||||
private static function getLock(Http $utopia): ResolverLock
|
||||
{
|
||||
$key = \spl_object_hash($utopia);
|
||||
if (!isset(self::$locks[$key])) {
|
||||
self::$locks[$key] = new ResolverLock();
|
||||
}
|
||||
|
||||
return self::$locks[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver for a given API {@see Route}.
|
||||
*
|
||||
@@ -24,34 +94,39 @@ class Resolvers
|
||||
Http $utopia,
|
||||
?Route $route,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$path = $route->getPath();
|
||||
foreach ($args as $key => $value) {
|
||||
if (\str_contains($path, '/:' . $key)) {
|
||||
$path = \str_replace(':' . $key, $value, $path);
|
||||
self::resolve(
|
||||
$utopia,
|
||||
$request,
|
||||
$response,
|
||||
$resolve,
|
||||
$reject,
|
||||
prepareRequest: static function (Request $request) use ($route, $args): void {
|
||||
$path = $route->getPath();
|
||||
foreach ($args as $key => $value) {
|
||||
if (\str_contains($path, '/:' . $key)) {
|
||||
$path = \str_replace(':' . $key, $value, $path);
|
||||
}
|
||||
}
|
||||
|
||||
$request->setMethod($route->getMethod());
|
||||
$request->setURI($path);
|
||||
|
||||
switch ($route->getMethod()) {
|
||||
case 'GET':
|
||||
$request->setQueryString($args);
|
||||
break;
|
||||
default:
|
||||
$request->setPayload($args);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$request->setMethod($route->getMethod());
|
||||
$request->setURI($path);
|
||||
|
||||
switch ($route->getMethod()) {
|
||||
case 'GET':
|
||||
$request->setQueryString($args);
|
||||
break;
|
||||
default:
|
||||
$request->setPayload($args);
|
||||
break;
|
||||
}
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,18 +166,23 @@ class Resolvers
|
||||
string $collectionId,
|
||||
callable $url,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$request->setMethod('GET');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
self::resolve(
|
||||
$utopia,
|
||||
$request,
|
||||
$response,
|
||||
$resolve,
|
||||
$reject,
|
||||
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void {
|
||||
$request->setMethod('GET');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,23 +202,29 @@ class Resolvers
|
||||
callable $url,
|
||||
callable $params,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$request->setMethod('GET');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
$request->setQueryString($params($databaseId, $collectionId, $args));
|
||||
$beforeResolve = function ($payload) {
|
||||
return $payload['documents'];
|
||||
};
|
||||
|
||||
$beforeResolve = function ($payload) {
|
||||
return $payload['documents'];
|
||||
};
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject, $beforeResolve);
|
||||
}
|
||||
);
|
||||
self::resolve(
|
||||
$utopia,
|
||||
$request,
|
||||
$response,
|
||||
$resolve,
|
||||
$reject,
|
||||
beforeResolve: $beforeResolve,
|
||||
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
|
||||
$request->setMethod('GET');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
$request->setQueryString($params($databaseId, $collectionId, $args));
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,19 +244,24 @@ class Resolvers
|
||||
callable $url,
|
||||
callable $params,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$request->setMethod('POST');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
$request->setPayload($params($databaseId, $collectionId, $args));
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
self::resolve(
|
||||
$utopia,
|
||||
$request,
|
||||
$response,
|
||||
$resolve,
|
||||
$reject,
|
||||
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
|
||||
$request->setMethod('POST');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
$request->setPayload($params($databaseId, $collectionId, $args));
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,19 +281,24 @@ class Resolvers
|
||||
callable $url,
|
||||
callable $params,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$request->setMethod('PATCH');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
$request->setPayload($params($databaseId, $collectionId, $args));
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
self::resolve(
|
||||
$utopia,
|
||||
$request,
|
||||
$response,
|
||||
$resolve,
|
||||
$reject,
|
||||
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
|
||||
$request->setMethod('PATCH');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
$request->setPayload($params($databaseId, $collectionId, $args));
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,18 +316,23 @@ class Resolvers
|
||||
string $collectionId,
|
||||
callable $url,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$request->setMethod('DELETE');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject);
|
||||
}
|
||||
);
|
||||
self::resolve(
|
||||
$utopia,
|
||||
$request,
|
||||
$response,
|
||||
$resolve,
|
||||
$reject,
|
||||
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void {
|
||||
$request->setMethod('DELETE');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,7 +342,7 @@ class Resolvers
|
||||
* @param callable $resolve
|
||||
* @param callable $reject
|
||||
* @param callable|null $beforeResolve
|
||||
* @param callable|null $beforeReject
|
||||
* @param callable|null $prepareRequest
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -252,39 +353,67 @@ class Resolvers
|
||||
callable $resolve,
|
||||
callable $reject,
|
||||
?callable $beforeResolve = null,
|
||||
?callable $beforeReject = null,
|
||||
?callable $prepareRequest = null,
|
||||
): void {
|
||||
// Drop json content type so post args are used directly
|
||||
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
|
||||
$request->removeHeader('content-type');
|
||||
}
|
||||
$lock = self::getLock($utopia);
|
||||
|
||||
$request = clone $request;
|
||||
$utopia->setResource('request', static fn () => $request);
|
||||
$response->setContentType(Response::CONTENT_TYPE_NULL);
|
||||
$response->clearSent();
|
||||
$lock->acquire();
|
||||
|
||||
$original = $utopia->getRoute();
|
||||
try {
|
||||
$route = $utopia->match($request, fresh: true);
|
||||
$request = clone $request;
|
||||
|
||||
$utopia->execute($route, $request, $response);
|
||||
} catch (\Throwable $e) {
|
||||
if ($beforeReject) {
|
||||
$e = $beforeReject($e);
|
||||
// Drop json content type so post args are used directly.
|
||||
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
|
||||
$request->removeHeader('content-type');
|
||||
}
|
||||
|
||||
if ($prepareRequest) {
|
||||
$prepareRequest($request);
|
||||
}
|
||||
|
||||
/** @var Response $resolverResponse */
|
||||
$resolverResponse = clone $utopia->getResource('response');
|
||||
$container = self::getResolverContainer($utopia);
|
||||
$container->set('request', static fn () => $request);
|
||||
$container->set('response', static fn () => $resolverResponse);
|
||||
$resolverResponse->setContentType(Response::CONTENT_TYPE_NULL);
|
||||
$resolverResponse->setSent(false);
|
||||
|
||||
$route = $utopia->match($request, fresh: true);
|
||||
$request->setRoute($route);
|
||||
|
||||
$utopia->execute($route, $request, $resolverResponse);
|
||||
|
||||
self::mergeResponseSideEffects($resolverResponse, $response);
|
||||
|
||||
if ($resolverResponse->isSent()) {
|
||||
$response
|
||||
->setStatusCode($resolverResponse->getStatusCode())
|
||||
->setSent(true);
|
||||
|
||||
$resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = $resolverResponse->getPayload();
|
||||
$statusCode = $resolverResponse->getStatusCode();
|
||||
} catch (\Throwable $e) {
|
||||
$reject($e);
|
||||
return;
|
||||
} finally {
|
||||
if ($original !== null) {
|
||||
$utopia->setRoute($original);
|
||||
}
|
||||
|
||||
$lock->release();
|
||||
unset(self::$locks[\spl_object_hash($utopia)]);
|
||||
}
|
||||
|
||||
$payload = $response->getPayload();
|
||||
|
||||
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) {
|
||||
if ($beforeReject) {
|
||||
$payload = $beforeReject($payload);
|
||||
}
|
||||
if ($statusCode < 200 || $statusCode >= 400) {
|
||||
$reject(new GQLException(
|
||||
message: $payload['message'],
|
||||
code: $response->getStatusCode()
|
||||
code: $statusCode
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
namespace Appwrite\GraphQL\Types;
|
||||
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
|
||||
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||
class Assoc extends Json
|
||||
{
|
||||
public $name = 'Assoc';
|
||||
public $description = 'The `Assoc` scalar type represents associative array values.';
|
||||
public string $name = 'Assoc';
|
||||
public ?string $description = 'The `Assoc` scalar type represents associative array values.';
|
||||
|
||||
public function serialize($value)
|
||||
{
|
||||
@@ -30,6 +31,10 @@ class Assoc extends Json
|
||||
|
||||
public function parseLiteral(Node $valueNode, ?array $variables = null)
|
||||
{
|
||||
return \json_decode($valueNode->value, true);
|
||||
if ($valueNode instanceof StringValueNode) {
|
||||
return \json_decode($valueNode->value, true);
|
||||
}
|
||||
|
||||
return parent::parseLiteral($valueNode, $variables);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ use GraphQL\Type\Definition\ScalarType;
|
||||
|
||||
class InputFile extends ScalarType
|
||||
{
|
||||
public $name = 'InputFile';
|
||||
public $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
|
||||
public string $name = 'InputFile';
|
||||
public ?string $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
|
||||
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
|
||||
|
||||
public function serialize($value)
|
||||
|
||||
@@ -14,8 +14,8 @@ use GraphQL\Type\Definition\ScalarType;
|
||||
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||
class Json extends ScalarType
|
||||
{
|
||||
public $name = 'Json';
|
||||
public $description = 'The `JSON` scalar type represents JSON values as specified by
|
||||
public string $name = 'Json';
|
||||
public ?string $description = 'The `JSON` scalar type represents JSON values as specified by
|
||||
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
|
||||
|
||||
public function serialize($value)
|
||||
|
||||
@@ -94,6 +94,7 @@ abstract class Migration
|
||||
'1.8.1' => 'V23',
|
||||
'1.9.0' => 'V24',
|
||||
'1.9.1' => 'V24',
|
||||
'1.9.2' => 'V24',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,8 +37,8 @@ class Delete extends Action
|
||||
->label('event', 'users.[userId].delete.mfa')
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('audits.resource', 'user/{user.$id}')
|
||||
->label('audits.userId', '{user.$id}')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'account',
|
||||
|
||||
@@ -170,11 +170,6 @@ class Create extends Action
|
||||
|
||||
$message = Template::fromFile($templatesPath . '/sms-base.tpl');
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['sms.mfaChallenge-' . $locale->default] ?? [];
|
||||
if (!empty($customTemplate)) {
|
||||
$message = $customTemplate['message'] ?? $message;
|
||||
}
|
||||
|
||||
$messageContent = Template::fromString($locale->getText("sms.verification.body"));
|
||||
$messageContent
|
||||
->setParam('{{project}}', $projectName)
|
||||
@@ -223,7 +218,9 @@ class Create extends Action
|
||||
$preview = $locale->getText("emails.mfaChallenge.preview");
|
||||
$heading = $locale->getText("emails.mfaChallenge.heading");
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->fallback] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
@@ -253,7 +250,8 @@ class Create extends Action
|
||||
|
||||
$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'])) {
|
||||
@@ -262,8 +260,13 @@ class Create extends Action
|
||||
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
|
||||
@@ -280,8 +283,13 @@ class Create extends Action
|
||||
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'] ?? '';
|
||||
@@ -289,7 +297,8 @@ class Create extends Action
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
|
||||
+24
-7
@@ -50,6 +50,11 @@ class Create extends Action
|
||||
return UtopiaResponse::MODEL_DOCUMENT_LIST;
|
||||
}
|
||||
|
||||
protected function getSupportForEmptyDocument()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
@@ -139,30 +144,42 @@ class Create extends Action
|
||||
->inject('eventProcessor')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
{
|
||||
$data = \is_string($data)
|
||||
? \json_decode($data, true)
|
||||
: $data;
|
||||
|
||||
$supportsEmptyDocument = $this->getSupportForEmptyDocument();
|
||||
$hasData = !empty($data);
|
||||
$hasDocuments = !empty($documents);
|
||||
|
||||
/**
|
||||
* Determine which internal path to call, single or bulk
|
||||
*/
|
||||
if (empty($data) && empty($documents)) {
|
||||
if (!$supportsEmptyDocument && !$hasData && !$hasDocuments) {
|
||||
// No single or bulk documents provided
|
||||
throw new Exception($this->getMissingDataException());
|
||||
}
|
||||
if (!empty($data) && !empty($documents)) {
|
||||
|
||||
// When empty documents are supported, an empty payload should still be treated as single create.
|
||||
if ($supportsEmptyDocument && !$hasData && !$hasDocuments) {
|
||||
$data = [];
|
||||
$hasData = true;
|
||||
}
|
||||
|
||||
if ($hasData && $hasDocuments) {
|
||||
// Both single and bulk documents provided
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSDKGroup());
|
||||
}
|
||||
if (!empty($data) && empty($documentId)) {
|
||||
if ($hasData && empty($documentId)) {
|
||||
// Single document provided without document ID
|
||||
$document = $this->isCollectionsAPI() ? 'Document' : 'Row';
|
||||
$message = "$document ID is required when creating a single " . strtolower($document) . '.';
|
||||
throw new Exception($this->getMissingDataException(), $message);
|
||||
}
|
||||
if (!empty($documents) && !empty($documentId)) {
|
||||
if ($hasDocuments && !empty($documentId)) {
|
||||
// Bulk documents provided with document ID
|
||||
$documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId';
|
||||
throw new Exception(
|
||||
@@ -170,13 +187,13 @@ class Create extends Action
|
||||
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSDKGroup() . ', set "$id" on each instead.'
|
||||
);
|
||||
}
|
||||
if (!empty($documents) && !empty($permissions)) {
|
||||
if ($hasDocuments && !empty($permissions)) {
|
||||
// Bulk documents provided with permissions
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSDKGroup() . ', set "$permissions" on each instead');
|
||||
}
|
||||
|
||||
$isBulk = true;
|
||||
if (!empty($data)) {
|
||||
$isBulk = $hasDocuments;
|
||||
if ($hasData) {
|
||||
// Single document provided, convert to single item array
|
||||
// But remember that it was single to respond with a single document
|
||||
$isBulk = false;
|
||||
|
||||
@@ -49,11 +49,6 @@ class Create extends Action
|
||||
$databaseOverride = '';
|
||||
$dbScheme = '';
|
||||
$databaseSharedTables = [];
|
||||
$databaseSharedTablesV1 = [];
|
||||
$databaseSharedTablesV2 = [];
|
||||
$projectSharedTables = [];
|
||||
$projectSharedTablesV1 = [];
|
||||
$projectSharedTablesV2 = [];
|
||||
|
||||
switch ($databasetype) {
|
||||
case DOCUMENTSDB:
|
||||
@@ -62,7 +57,6 @@ class Create extends Action
|
||||
$databaseOverride = System::getEnv('_APP_DATABASE_DOCUMENTSDB_OVERRIDE');
|
||||
$dbScheme = System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb');
|
||||
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', '')));
|
||||
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', '')));
|
||||
break;
|
||||
case VECTORSDB:
|
||||
$databases = Config::getParam('pools-vectorsdb', []);
|
||||
@@ -70,7 +64,6 @@ class Create extends Action
|
||||
$databaseOverride = System::getEnv('_APP_DATABASE_VECTORSDB_OVERRIDE');
|
||||
$dbScheme = System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql');
|
||||
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', '')));
|
||||
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', '')));
|
||||
break;
|
||||
default:
|
||||
// legacy/tablesdb
|
||||
@@ -78,8 +71,7 @@ class Create extends Action
|
||||
return $dsn;
|
||||
}
|
||||
|
||||
$isSharedTablesV1 = false;
|
||||
$isSharedTablesV2 = false;
|
||||
$isSharedTables = false;
|
||||
|
||||
if (!empty($dsn)) {
|
||||
try {
|
||||
@@ -90,10 +82,7 @@ class Create extends Action
|
||||
}
|
||||
|
||||
$projectSharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$projectSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
|
||||
$projectSharedTablesV2 = \array_diff($projectSharedTables, $projectSharedTablesV1);
|
||||
$isSharedTablesV1 = \in_array($dsnHost, $projectSharedTablesV1);
|
||||
$isSharedTablesV2 = \in_array($dsnHost, $projectSharedTablesV2);
|
||||
$isSharedTables = \in_array($dsnHost, $projectSharedTables);
|
||||
}
|
||||
|
||||
if ($region !== 'default') {
|
||||
@@ -102,18 +91,14 @@ class Create extends Action
|
||||
return str_contains($value, $region);
|
||||
});
|
||||
}
|
||||
$databaseSharedTablesV2 = \array_diff($databaseSharedTables, $databaseSharedTablesV1);
|
||||
|
||||
$index = \array_search($databaseOverride, $databases);
|
||||
if ($index !== false) {
|
||||
$selectedDsn = $databases[$index];
|
||||
} else {
|
||||
if (!empty($dsn) && !empty($databaseSharedTables)) {
|
||||
$beforeFilter = \array_values($databases);
|
||||
if ($isSharedTablesV1) {
|
||||
$databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV1));
|
||||
} elseif ($isSharedTablesV2) {
|
||||
$databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV2));
|
||||
if ($isSharedTables) {
|
||||
$databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTables));
|
||||
} else {
|
||||
$databases = array_filter($databases, fn ($value) => !\in_array($value, $databaseSharedTables));
|
||||
}
|
||||
|
||||
+6
@@ -34,6 +34,12 @@ class Create extends DocumentCreate
|
||||
return UtopiaResponse::MODEL_DOCUMENT_LIST;
|
||||
}
|
||||
|
||||
protected function getSupportForEmptyDocument()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
|
||||
class XList extends DocumentLogXList
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listDocumentsDBDocumentLogs';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
|
||||
->desc('List document logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'documents.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'documentsDB',
|
||||
group: 'logs',
|
||||
name: 'listDocumentLogs',
|
||||
description: '/docs/references/documentsdb/get-document-logs.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON,
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('documentId', '', new UID(), 'Document ID.')
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('authorization')
|
||||
->inject('audit')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
|
||||
class XList extends CollectionLogXList
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listDocumentsDBCollectionLogs';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/logs')
|
||||
->desc('List collection logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'documentsDB',
|
||||
group: $this->getSdkGroup(),
|
||||
name: 'listCollectionLogs',
|
||||
description: '/docs/references/documentsdb/get-collection-logs.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
|
||||
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('authorization')
|
||||
->inject('audit')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
|
||||
class XList extends DocumentLogXList
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listVectorsDBDocumentLogs';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
|
||||
->desc('List document logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'documents.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vectorsDB',
|
||||
group: 'logs',
|
||||
name: 'listDocumentLogs',
|
||||
description: '/docs/references/vectorsdb/get-document-logs.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON,
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('documentId', '', new UID(), 'Document ID.')
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('authorization')
|
||||
->inject('audit')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
|
||||
class XList extends CollectionLogXList
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listVectorsDBCollectionLogs';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/logs')
|
||||
->desc('List collection logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vectorsDB',
|
||||
group: $this->getSdkGroup(),
|
||||
name: 'listCollectionLogs',
|
||||
description: '/docs/references/vectorsdb/get-collection-logs.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('authorization')
|
||||
->inject('audit')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\B
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Create as CreateRow;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Delete as DeleteRow;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Get as GetRow;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs\XList as ListRowLogs;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Update as UpdateRow;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Upsert as UpsertRow;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\XList as ListRows;
|
||||
@@ -21,7 +20,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Cre
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Delete as DeleteColumnIndex;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Get as GetColumnIndex;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\XList as ListColumnIndexes;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs\XList as ListTableLogs;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Update as UpdateTable;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Usage\Get as GetTableUsage;
|
||||
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\XList as ListTables;
|
||||
@@ -69,7 +67,6 @@ class DocumentsDB extends Base
|
||||
$service->addAction(UpdateTable::getName(), new UpdateTable());
|
||||
$service->addAction(DeleteTable::getName(), new DeleteTable());
|
||||
$service->addAction(ListTables::getName(), new ListTables());
|
||||
$service->addAction(ListTableLogs::getName(), new ListTableLogs());
|
||||
$service->addAction(GetTableUsage::getName(), new GetTableUsage());
|
||||
}
|
||||
|
||||
@@ -92,7 +89,6 @@ class DocumentsDB extends Base
|
||||
$service->addAction(DeleteRow::getName(), new DeleteRow());
|
||||
$service->addAction(DeleteRows::getName(), new DeleteRows());
|
||||
$service->addAction(ListRows::getName(), new ListRows());
|
||||
$service->addAction(ListRowLogs::getName(), new ListRowLogs());
|
||||
$service->addAction(IncrementRowColumn::getName(), new IncrementRowColumn());
|
||||
$service->addAction(DecrementRowColumn::getName(), new DecrementRowColumn());
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Bul
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Create as CreateDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Delete as DeleteDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Get as GetDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs\XList as ListDocumentLogs;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Update as UpdateDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Upsert as UpsertDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\XList as ListDocuments;
|
||||
@@ -19,7 +18,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Creat
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Delete as DeleteIndex;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Get as GetIndex;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\XList as ListIndexes;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs\XList as ListCollectionLogs;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Update as UpdateCollection;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Usage\Get as GetCollectionUsage;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\XList as ListCollections;
|
||||
@@ -69,7 +67,6 @@ class VectorsDB extends Base
|
||||
$service->addAction(UpdateCollection::getName(), new UpdateCollection());
|
||||
$service->addAction(DeleteCollection::getName(), new DeleteCollection());
|
||||
$service->addAction(ListCollections::getName(), new ListCollections());
|
||||
$service->addAction(ListCollectionLogs::getName(), new ListCollectionLogs());
|
||||
$service->addAction(GetCollectionUsage::getName(), new GetCollectionUsage());
|
||||
}
|
||||
|
||||
@@ -92,7 +89,6 @@ class VectorsDB extends Base
|
||||
$service->addAction(UpdateDocuments::getName(), new UpdateDocuments());
|
||||
$service->addAction(UpsertDocuments::getName(), new UpsertDocuments());
|
||||
$service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
|
||||
$service->addAction(ListDocumentLogs::getName(), new ListDocumentLogs());
|
||||
}
|
||||
|
||||
private function registerTransactionActions(Service $service): void
|
||||
|
||||
@@ -31,7 +31,7 @@ class Get extends Action
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/download')
|
||||
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build/download', ['type' => 'output'])
|
||||
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build/download')
|
||||
->groups(['api', 'functions'])
|
||||
->desc('Get deployment download')
|
||||
->label('scope', 'functions.read')
|
||||
|
||||
@@ -6,9 +6,9 @@ use Ahc\Jwt\JWT;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Message\Usage as UsageMessage;
|
||||
use Appwrite\Event\Publisher\Screenshot;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
|
||||
use Appwrite\Usage\Context;
|
||||
@@ -58,7 +58,7 @@ class Builds extends Action
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForScreenshots')
|
||||
->inject('publisherForScreenshots')
|
||||
->inject('queueForWebhooks')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForRealtime')
|
||||
@@ -84,7 +84,7 @@ class Builds extends Action
|
||||
Document $project,
|
||||
Database $dbForPlatform,
|
||||
Event $queueForEvents,
|
||||
Screenshot $queueForScreenshots,
|
||||
Screenshot $publisherForScreenshots,
|
||||
Webhook $queueForWebhooks,
|
||||
Func $queueForFunctions,
|
||||
Realtime $queueForRealtime,
|
||||
@@ -126,7 +126,7 @@ class Builds extends Action
|
||||
$deviceForFunctions,
|
||||
$deviceForSites,
|
||||
$deviceForFiles,
|
||||
$queueForScreenshots,
|
||||
$publisherForScreenshots,
|
||||
$queueForWebhooks,
|
||||
$queueForFunctions,
|
||||
$queueForRealtime,
|
||||
@@ -144,7 +144,8 @@ class Builds extends Action
|
||||
$log,
|
||||
$executor,
|
||||
$plan,
|
||||
$platform
|
||||
$platform,
|
||||
(int) ($payload['timeout'] ?? System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900))
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -161,7 +162,7 @@ class Builds extends Action
|
||||
Device $deviceForFunctions,
|
||||
Device $deviceForSites,
|
||||
Device $deviceForFiles,
|
||||
Screenshot $queueForScreenshots,
|
||||
Screenshot $publisherForScreenshots,
|
||||
Webhook $queueForWebhooks,
|
||||
Func $queueForFunctions,
|
||||
Realtime $queueForRealtime,
|
||||
@@ -179,7 +180,8 @@ class Builds extends Action
|
||||
Log $log,
|
||||
Executor $executor,
|
||||
array $plan,
|
||||
array $platform
|
||||
array $platform,
|
||||
int $timeout
|
||||
): void {
|
||||
Console::info('Deployment action started');
|
||||
|
||||
@@ -592,10 +594,7 @@ class Builds extends Action
|
||||
|
||||
$cpus = $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT;
|
||||
$memory = max($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, $minMemory);
|
||||
$timeout = (int) System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900);
|
||||
|
||||
$jwtExpiry = (int) System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900);
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $timeout, 0);
|
||||
|
||||
$apiKey = $jwtObj->encode([
|
||||
'projectId' => $project->getId(),
|
||||
@@ -1120,10 +1119,10 @@ class Builds extends Action
|
||||
|
||||
/** Screenshot site */
|
||||
if ($resource->getCollection() === 'sites') {
|
||||
$queueForScreenshots
|
||||
->setDeploymentId($deployment->getId())
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$publisherForScreenshots->enqueue(new \Appwrite\Event\Message\Screenshot(
|
||||
project: $project,
|
||||
deploymentId: $deployment->getId(),
|
||||
));
|
||||
|
||||
Console::log('Site screenshot queued');
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Modules\Functions\Workers;
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Event\Message\Screenshot;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Permission;
|
||||
use Appwrite\Role;
|
||||
@@ -62,9 +63,11 @@ class Screenshots extends Action
|
||||
throw new \Exception('Missing payload');
|
||||
}
|
||||
|
||||
$screenshotMessage = Screenshot::fromArray($payload);
|
||||
|
||||
Console::log('Site screenshot started');
|
||||
|
||||
$deploymentId = $payload['deploymentId'] ?? null;
|
||||
$deploymentId = $screenshotMessage->deploymentId;
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Audits;
|
||||
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Publisher\Audit;
|
||||
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
@@ -42,16 +42,16 @@ class Get extends Base
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
|
||||
->inject('queueForAudits')
|
||||
->inject('publisherForAudits')
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(int|string $threshold, Audit $queueForAudits, Response $response): void
|
||||
public function action(int|string $threshold, Audit $publisherForAudits, Response $response): void
|
||||
{
|
||||
$threshold = (int) $threshold;
|
||||
|
||||
$size = $queueForAudits->getSize();
|
||||
$size = $publisherForAudits->getSize();
|
||||
|
||||
$this->assertQueueThreshold($size, $threshold);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Certificates;
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
@@ -42,16 +42,16 @@ class Get extends Base
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(int|string $threshold, Certificate $queueForCertificates, Response $response): void
|
||||
public function action(int|string $threshold, Certificate $publisherForCertificates, Response $response): void
|
||||
{
|
||||
$threshold = (int) $threshold;
|
||||
|
||||
$size = $queueForCertificates->getSize();
|
||||
$size = $publisherForCertificates->getSize();
|
||||
|
||||
$this->assertQueueThreshold($size, $threshold);
|
||||
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Failed;
|
||||
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Publisher\Audit;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
|
||||
use Appwrite\Event\Publisher\Screenshot;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -75,17 +75,17 @@ class Get extends Base
|
||||
->inject('response')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForAudits')
|
||||
->inject('publisherForAudits')
|
||||
->inject('queueForMails')
|
||||
->inject('queueForFunctions')
|
||||
->inject('publisherForStatsResources')
|
||||
->inject('publisherForUsage')
|
||||
->inject('queueForWebhooks')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('queueForBuilds')
|
||||
->inject('queueForMessaging')
|
||||
->inject('publisherForMigrations')
|
||||
->inject('queueForScreenshots')
|
||||
->inject('publisherForScreenshots')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -95,32 +95,32 @@ class Get extends Base
|
||||
Response $response,
|
||||
Database $queueForDatabase,
|
||||
Delete $queueForDeletes,
|
||||
Audit $queueForAudits,
|
||||
Audit $publisherForAudits,
|
||||
Mail $queueForMails,
|
||||
Func $queueForFunctions,
|
||||
StatsResourcesPublisher $publisherForStatsResources,
|
||||
UsagePublisher $publisherForUsage,
|
||||
Webhook $queueForWebhooks,
|
||||
Certificate $queueForCertificates,
|
||||
Certificate $publisherForCertificates,
|
||||
Build $queueForBuilds,
|
||||
Messaging $queueForMessaging,
|
||||
MigrationPublisher $publisherForMigrations,
|
||||
Screenshot $queueForScreenshots,
|
||||
Screenshot $publisherForScreenshots,
|
||||
): void {
|
||||
$threshold = (int) $threshold;
|
||||
|
||||
$queue = match ($name) {
|
||||
System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME) => $queueForDatabase,
|
||||
System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME) => $queueForDeletes,
|
||||
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $queueForAudits,
|
||||
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $publisherForAudits,
|
||||
System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $queueForMails,
|
||||
System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $queueForFunctions,
|
||||
System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME) => $publisherForStatsResources,
|
||||
System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME) => $publisherForUsage,
|
||||
System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks,
|
||||
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $queueForCertificates,
|
||||
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $publisherForCertificates,
|
||||
System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds,
|
||||
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $queueForScreenshots,
|
||||
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $publisherForScreenshots,
|
||||
System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging,
|
||||
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $publisherForMigrations,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Logs;
|
||||
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Publisher\Audit;
|
||||
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
@@ -42,16 +42,16 @@ class Get extends Base
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
|
||||
->inject('queueForAudits')
|
||||
->inject('publisherForAudits')
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(int|string $threshold, Audit $queueForAudits, Response $response): void
|
||||
public function action(int|string $threshold, Audit $publisherForAudits, Response $response): void
|
||||
{
|
||||
$threshold = (int) $threshold;
|
||||
|
||||
$size = $queueForAudits->getSize();
|
||||
$size = $publisherForAudits->getSize();
|
||||
|
||||
$this->assertQueueThreshold($size, $threshold);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Text;
|
||||
@@ -31,7 +32,7 @@ class Update extends Action
|
||||
->desc('Update project labels')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('event', 'labels.*.update')
|
||||
// ->label('event', 'project.labels.update')
|
||||
->label('audits.event', 'project.labels.update')
|
||||
->label('audits.resource', 'project.labels/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
@@ -53,6 +54,7 @@ class Update extends Action
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -63,11 +65,12 @@ class Update extends Action
|
||||
array $labels,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project
|
||||
Document $project,
|
||||
Authorization $authorization
|
||||
): void {
|
||||
$labels = (array) \array_values(\array_unique($labels));
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels]));
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels])));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class Delete extends Action
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].delete')
|
||||
->label('audits.event', 'project.platform.delete')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('audits.resource', 'project.platform/{request.platformId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
|
||||
@@ -139,7 +139,7 @@ class Create extends Action
|
||||
if (empty($key) && empty($type)) {
|
||||
// Modern request, validate hostname
|
||||
if (empty($hostname)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "hostname" is not optional.');
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Param "hostname" is not optional.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-9
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status;
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Protocols;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -21,27 +22,28 @@ class Update extends Action
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectProtocolStatus';
|
||||
return 'updateProjectProtocol';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/protocols/:protocolId/status')
|
||||
->setHttpPath('/v1/project/protocols/:protocolId')
|
||||
->httpAlias('/v1/project/protocols/:protocolId/status')
|
||||
->httpAlias('/v1/projects/:projectId/api')
|
||||
->desc('Update project protocol status')
|
||||
->desc('Update project protocol')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('event', 'protocols.[protocol].update')
|
||||
->label('audits.event', 'project.protocols.[protocol].update')
|
||||
->label('event', 'protocols.[protocolId].update')
|
||||
->label('audits.event', 'project.protocols.[protocolId].update')
|
||||
->label('audits.resource', 'project.protocols/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'updateProtocolStatus',
|
||||
name: 'updateProtocol',
|
||||
description: <<<EOT
|
||||
Update the status of a specific protocol. Use this endpoint to enable or disable a protocol in your project.
|
||||
Update properties of a specific protocol. Use this endpoint to enable or disable a protocol in your project.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
@@ -57,6 +59,7 @@ class Update extends Action
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -66,7 +69,8 @@ class Update extends Action
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
$protocols = $project->getAttribute('apis', []);
|
||||
$protocols[$protocolId] = $enabled;
|
||||
@@ -75,6 +79,8 @@ class Update extends Action
|
||||
'apis' => $protocols,
|
||||
])));
|
||||
|
||||
$queueForEvents->setParam('protocolId', $protocolId);
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\SMTP\Tests;
|
||||
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Extend\Exception as Exception;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createProjectSMTPTest';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/project/smtp/tests')
|
||||
->httpAlias('/v1/projects/:projectId/smtp/tests')
|
||||
->desc('Create project SMTP test')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'smtp',
|
||||
name: 'createSMTPTest',
|
||||
description: <<<EOT
|
||||
Send a test email to verify SMTP configuration.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_NOCONTENT,
|
||||
model: UtopiaResponse::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE,
|
||||
))
|
||||
->param('emails', [], new ArrayList(new Email(), 10), 'Array of emails to send test email to. Maximum of 10 emails are allowed.')
|
||||
->param('senderName', '', new Text(256), 'Name of the email sender', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('senderEmail', '', new Email(), 'Email of the sender', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('replyTo', '', new Email(), 'Reply to email', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('host', '', new Hostname(), 'SMTP server host name', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('port', null, new Integer(), 'SMTP server port', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('username', '', new Text(256), 'SMTP server username', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('password', '', new Text(256), 'SMTP server password', optional: true, deprecated: true) // Backwards compatibility
|
||||
->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', optional: true, deprecated: true) // Backwards compatibility
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForMails')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $emails
|
||||
*/
|
||||
public function action(
|
||||
array $emails,
|
||||
string $paramSenderName, // Backwards compatibility
|
||||
string $paramSenderEmail, // Backwards compatibility
|
||||
string $paramReplyTo, // Backwards compatibility
|
||||
string $paramHost, // Backwards compatibility
|
||||
?int $paramPort, // Backwards compatibility
|
||||
string $paramUsername, // Backwards compatibility
|
||||
string $paramPassword, // Backwards compatibility
|
||||
string $paramSecure, // Backwards compatibility
|
||||
Response $response,
|
||||
Document $project,
|
||||
Mail $queueForMails,
|
||||
array $plan
|
||||
): void {
|
||||
// Backwards compatibility: use inline params if provided, otherwise fall back to project SMTP config.
|
||||
// When inline params are provided they are treated as self-contained — project config is ignored
|
||||
// so legacy (1.9.1) callers do not get project state (e.g. replyToName) leaked into their request.
|
||||
$hasInlineParams = !empty($paramHost);
|
||||
|
||||
$smtp = $project->getAttribute('smtp', []);
|
||||
|
||||
if (!$hasInlineParams && ($smtp['enabled'] ?? false) !== true) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'SMTP must be enabled on the project to send a test email.');
|
||||
}
|
||||
|
||||
if ($hasInlineParams) {
|
||||
$senderName = $paramSenderName;
|
||||
$senderEmail = $paramSenderEmail;
|
||||
$replyToEmail = $paramReplyTo;
|
||||
$replyToName = ''; // 1.9.1 inline params did not include replyToName
|
||||
$host = $paramHost;
|
||||
$port = $paramPort ?? 0;
|
||||
$username = $paramUsername;
|
||||
$password = $paramPassword;
|
||||
$secure = $paramSecure;
|
||||
} else {
|
||||
$senderName = $smtp['senderName'] ?? '';
|
||||
$senderEmail = $smtp['senderEmail'] ?? '';
|
||||
$replyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? ''; // Includes backwards compatibility
|
||||
$replyToName = $smtp['replyToName'] ?? '';
|
||||
$host = $smtp['host'] ?? '';
|
||||
$port = $smtp['port'] ?? 0;
|
||||
$username = $smtp['username'] ?? '';
|
||||
$password = $smtp['password'] ?? '';
|
||||
$secure = $smtp['secure'] ?? '';
|
||||
}
|
||||
|
||||
if (empty($senderEmail)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'SMTP sender email must be configured on the project to send a test email.');
|
||||
}
|
||||
|
||||
if (empty($host)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'SMTP host must be configured on the project to send a test email.');
|
||||
}
|
||||
|
||||
if (empty($port)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'SMTP port must be configured on the project to send a test email.');
|
||||
}
|
||||
|
||||
// Fallback to sender details when reply-to is not explicitly configured
|
||||
$replyToEmailDisplay = !empty($replyToEmail) ? $replyToEmail : $senderEmail;
|
||||
$replyToNameDisplay = !empty($replyToName) ? $replyToName : $senderName;
|
||||
|
||||
$subject = 'Custom SMTP email sample';
|
||||
$template = Template::fromFile(APP_CE_CONFIG_DIR . '/locale/templates/email-smtp-test.tpl');
|
||||
$template
|
||||
->setParam('{{from}}', "{$senderName} ({$senderEmail})")
|
||||
->setParam('{{replyTo}}', "{$replyToNameDisplay} ({$replyToEmailDisplay})")
|
||||
->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)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName)
|
||||
->setRecipient($email)
|
||||
->setName('')
|
||||
->setBodyTemplate(APP_CE_CONFIG_DIR . '/locale/templates/email-base-styled.tpl')
|
||||
->setBody($template->render())
|
||||
->setVariables([])
|
||||
->setSubject($subject)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\SMTP;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Throwable;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectSMTP';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/smtp')
|
||||
->httpAlias('/v1/projects/:projectId/smtp')
|
||||
->desc('Update project SMTP configuration')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
// ->label('event', 'project.smtp.update')
|
||||
->label('audits.event', 'project.smtp.update')
|
||||
->label('audits.resource', 'project.smtp/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'smtp',
|
||||
name: 'updateSMTP',
|
||||
description: <<<EOT
|
||||
Update the SMTP configuration for your project. Use this endpoint to configure your project's SMTP provider with your custom settings for sending transactional emails.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('host', null, new Nullable(new Hostname()), 'SMTP server hostname (domain)', optional: true)
|
||||
->param('port', null, new Nullable(new Integer()), 'SMTP server port', optional: true)
|
||||
->param('username', null, new Nullable(new Text(256)), 'SMTP server username. Leave empty for no authorization.', optional: true)
|
||||
->param('password', null, new Nullable(new Text(256)), 'SMTP server password. Leave empty for no authorization. This property is stored securely and cannot be read in future (write-only).', optional: true)
|
||||
->param('senderEmail', null, new Nullable(new Email()), 'Email address shown in inbox as the sender of the email.', optional: true)
|
||||
->param('senderName', null, new Nullable(new Text(256)), 'Name shown in inbox as the sender of the email.', optional: true)
|
||||
->param('replyToEmail', null, new Nullable(new Email()), 'Email used when user replies to the email.', optional: true)
|
||||
->param('replyToName', null, new Nullable(new Text(256)), 'Name used when user replies to the email.', optional: true)
|
||||
->param('secure', null, new Nullable(new WhiteList(['tls', 'ssl'], true)), 'Configures if communication with SMTP server is encrypted. Allowed values are: tls, ssl. Leave empty for no encryption.', optional: true)
|
||||
->param('enabled', null, new Nullable(new Boolean()), 'Enable or disable custom SMTP. Custom SMTP is useful for branding purposes, but also allows use of custom email templates.', optional: true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
|
||||
public function action(
|
||||
?string $host,
|
||||
?int $port,
|
||||
?string $username,
|
||||
?string $password,
|
||||
?string $senderEmail,
|
||||
?string $senderName,
|
||||
?string $replyToEmail,
|
||||
?string $replyToName,
|
||||
?string $secure,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization
|
||||
): void {
|
||||
// Fetch current configuration
|
||||
$smtp = $project->getAttribute('smtp', []);
|
||||
|
||||
// Apply changes
|
||||
$keys = ['host', 'port', 'username', 'password', 'senderEmail', 'senderName', 'replyToEmail', 'replyToName', 'secure', 'enabled'];
|
||||
foreach ($keys as $key) {
|
||||
if (!\is_null(${$key})) {
|
||||
$smtp[$key] = ${$key};
|
||||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
$smtp['replyToEmail'] = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
|
||||
|
||||
if (($smtp['enabled'] ?? false) === true) {
|
||||
// Ensure required fields are set
|
||||
$requiredKeys = ['host', 'port', 'senderEmail'];
|
||||
foreach ($requiredKeys as $key) {
|
||||
if (empty($smtp[$key])) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Param "' . $key . '" is not optional.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate SMTP credentials
|
||||
// Validate when the caller is explicitly enabling or hasn't expressed a preference
|
||||
// (so a credentials-only PATCH can auto-enable). Skip only when the caller is
|
||||
// explicitly keeping/turning SMTP off.
|
||||
if (\is_null($enabled) || $enabled === true) {
|
||||
$mail = new PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
|
||||
$mail->Host = $smtp['host'] ?? '';
|
||||
$mail->Port = $smtp['port'] ?? '';
|
||||
$mail->SMTPSecure = $smtp['secure'] ?? '';
|
||||
$mail->setFrom($smtp['senderEmail'], $smtp['senderName'] ?? '');
|
||||
|
||||
if (!empty($smtp['username'] ?? '')) {
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $smtp['username'];
|
||||
$mail->Password = $smtp['password'] ?? '';
|
||||
}
|
||||
|
||||
if (!empty($smtp['replyToEmail'] ?? '')) {
|
||||
$mail->addReplyTo($smtp['replyToEmail'], $smtp['replyToName'] ?? '');
|
||||
}
|
||||
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->Timeout = 5;
|
||||
|
||||
try {
|
||||
$valid = $mail->SmtpConnect();
|
||||
|
||||
if (!$valid) {
|
||||
throw new \Exception('Connection is not valid.');
|
||||
}
|
||||
|
||||
// Auto-enable if configuration is valid
|
||||
// Dont do this if specifically request to mark disabled
|
||||
if (\is_null($enabled)) {
|
||||
$smtp['enabled'] = true;
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
if (($smtp['enabled'] ?? null) === true) {
|
||||
throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, $error->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
$updates = new Document([
|
||||
'smtp' => $smtp,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
+15
-9
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Services\Status;
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Services;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -21,27 +22,28 @@ class Update extends Action
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectServiceStatus';
|
||||
return 'updateProjectService';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/services/:serviceId/status')
|
||||
->setHttpPath('/v1/project/services/:serviceId')
|
||||
->httpAlias('/v1/project/services/:serviceId/status')
|
||||
->httpAlias('/v1/projects/:projectId/service')
|
||||
->desc('Update project service status')
|
||||
->desc('Update project service')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('event', 'services.[service].update')
|
||||
->label('audits.event', 'project.services.[service].update')
|
||||
->label('event', 'services.[serviceId].update')
|
||||
->label('audits.event', 'project.services.[serviceId].update')
|
||||
->label('audits.resource', 'project.services/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'updateServiceStatus',
|
||||
name: 'updateService',
|
||||
description: <<<EOT
|
||||
Update the status of a specific service. Use this endpoint to enable or disable a service in your project.
|
||||
Update properties of a specific service. Use this endpoint to enable or disable a service in your project.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
@@ -57,6 +59,7 @@ class Update extends Action
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -66,7 +69,8 @@ class Update extends Action
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents
|
||||
): void {
|
||||
$services = $project->getAttribute('services', []);
|
||||
$services[$serviceId] = $enabled;
|
||||
@@ -75,6 +79,8 @@ class Update extends Action
|
||||
'services' => $services,
|
||||
])));
|
||||
|
||||
$queueForEvents->setParam('serviceId', $serviceId);
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Templates\Email;
|
||||
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getProjectEmailTemplate';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/project/templates/email/:templateId')
|
||||
->httpAlias('/v1/projects/:projectId/templates/email/:templateId/:locale')
|
||||
->desc('Get project email template')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'templates.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'templates',
|
||||
name: 'getEmailTemplate',
|
||||
description: <<<EOT
|
||||
Get a custom email template for the specified locale and type. This endpoint returns the template content, subject, and other configuration details.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_EMAIL_TEMPLATE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Custom email template type. Can be one of: '.\implode(', ', Config::getParam('locale-templates')['email'] ?? []))
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Custom email template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $templateId,
|
||||
string $locale,
|
||||
Response $response,
|
||||
Document $project,
|
||||
) {
|
||||
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
// Get custom template if available
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['email.' . $templateId . '-' . $locale] ?? [];
|
||||
|
||||
// Enforced params
|
||||
$template['templateId'] = $templateId;
|
||||
$template['locale'] = $locale;
|
||||
|
||||
// Prepare default tempaltes
|
||||
$localeObj = new Locale($locale);
|
||||
$localeObj->setFallback(System::getEnv('_APP_LOCALE', 'en'));
|
||||
|
||||
$defaultSubject = $localeObj->getText('emails.' . $templateId . '.subject');
|
||||
$defaultMessage = $this->getDefaultMessage($templateId, $localeObj);
|
||||
|
||||
// Apply defaults if needed
|
||||
if (\is_null($template['message'] ?? null)) {
|
||||
$template['message'] = $defaultMessage;
|
||||
}
|
||||
|
||||
if (\is_null($template['subject'] ?? null)) {
|
||||
$template['subject'] = $defaultSubject;
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (!\is_null($template['replyTo'] ?? null)) {
|
||||
$template['replyToEmail'] = $template['replyToEmail'] ?? $template['replyTo'] ?? '';
|
||||
}
|
||||
|
||||
$response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE);
|
||||
}
|
||||
|
||||
protected function getDefaultMessage(string $templateId, Locale $localeObj): string
|
||||
{
|
||||
$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[$templateId] ?? [
|
||||
'file' => 'email-inner-base.tpl',
|
||||
'placeholders' => ['buttonText', 'body', 'footer']
|
||||
];
|
||||
|
||||
$templateString = file_get_contents(APP_CE_CONFIG_DIR . '/locale/templates/' . $config['file']);
|
||||
$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.{$templateId}.{$param}"), escapeHtml: $escapeHtml);
|
||||
}
|
||||
|
||||
$message
|
||||
->setParam('{{hello}}', $localeObj->getText("emails.{$templateId}.hello"))
|
||||
->setParam('{{thanks}}', $localeObj->getText("emails.{$templateId}.thanks"))
|
||||
->setParam('{{signature}}', $localeObj->getText("emails.{$templateId}.signature"));
|
||||
|
||||
$message = $message->render(useContent: true);
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Templates\Email;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectEmailTemplate';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/templates/email')
|
||||
->httpAlias('/v1/projects/:projectId/templates/email')
|
||||
->httpAlias('/v1/projects/:projectId/templates/email/:templateId/:locale')
|
||||
->desc('Update project email template')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'templates.write')
|
||||
->label('event', 'templates.[templateId].update')
|
||||
->label('audits.event', 'project.template.update')
|
||||
->label('audits.resource', 'project.template/{response.templateId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'templates',
|
||||
name: 'updateEmailTemplate',
|
||||
description: <<<EOT
|
||||
Update a custom email template for the specified locale and type. Use this endpoint to modify the content of your email templates.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_EMAIL_TEMPLATE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Custom email template type. Can be one of: '.\implode(', ', Config::getParam('locale-templates')['email'] ?? []))
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Custom email template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'])
|
||||
->param('subject', null, new Nullable(new Text(255)), 'Subject of the email template. Can be up to 255 characters.', optional: true)
|
||||
->param('message', null, new Nullable(new Text(10485760)), 'Plain or HTML body of the email template message. Can be up to 10MB of content.', optional: true)
|
||||
->param('senderName', null, new Nullable(new Text(255, 0)), 'Name of the email sender.', optional: true)
|
||||
->param('senderEmail', null, new Nullable(new Email()), 'Email of the sender.', optional: true)
|
||||
->param('replyToEmail', null, new Nullable(new Email()), 'Reply to email.', optional: true)
|
||||
->param('replyToName', null, new Nullable(new Text(255, 0)), 'Reply to name.', optional: true)
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $templateId,
|
||||
string $locale,
|
||||
?string $subject,
|
||||
?string $message,
|
||||
?string $senderName,
|
||||
?string $senderEmail,
|
||||
?string $replyToEmail,
|
||||
?string $replyToName,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
) {
|
||||
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
// Prevent template update if custom SMTP is not configured
|
||||
$smtp = $project->getAttribute('smtp', []);
|
||||
if (($smtp['enabled'] ?? false) !== true) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'SMTP must be enabled on the project to configure custom email templates.');
|
||||
}
|
||||
|
||||
// Fetch current configuration
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['email.' . $templateId . '-' . $locale] ?? [];
|
||||
|
||||
// Apply changes
|
||||
$keys = ['senderName', 'senderEmail', 'replyToEmail', 'replyToName', 'message', 'subject'];
|
||||
foreach ($keys as $key) {
|
||||
if (!\is_null(${$key})) {
|
||||
$template[$key] = ${$key};
|
||||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (!\is_null($template['replyTo'] ?? null)) {
|
||||
$template['replyToEmail'] = $template['replyToEmail'] ?? $template['replyTo'] ?? '';
|
||||
}
|
||||
|
||||
// Ensure required fields are set
|
||||
$requiredKeys = ['subject', 'message'];
|
||||
foreach ($requiredKeys as $key) {
|
||||
if (empty($template[$key])) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Param "' . $key . '" is not optional.');
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
$templates['email.' . $templateId . '-' . $locale] = $template;
|
||||
$updates = new Document([
|
||||
'templates' => $templates,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents->setParam('templateId', $templateId);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'templateId' => $templateId,
|
||||
'locale' => $locale,
|
||||
'subject' => $template['subject'],
|
||||
'message' => $template['message'],
|
||||
'senderName' => $template['senderName'] ?? '',
|
||||
'senderEmail' => $template['senderEmail'] ?? '',
|
||||
'replyToEmail' => $template['replyToEmail'] ?? '',
|
||||
'replyToName' => $template['replyToName'] ?? '',
|
||||
]), Response::MODEL_EMAIL_TEMPLATE);
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,12 @@ use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Update as Updat
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Create as CreateWindowsPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Update as UpdateWindowsPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatforms;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status\Update as UpdateProjectProtocolStatus;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Services\Status\Update as UpdateProjectServiceStatus;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Protocols\Update as UpdateProjectProtocol;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Services\Update as UpdateProjectService;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\SMTP\Tests\Create as CreateSMTPTest;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\SMTP\Update as UpdateSMTP;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Templates\Email\Get as GetTemplate;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Templates\Email\Update as UpdateTemplate;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
|
||||
@@ -42,8 +46,16 @@ class Http extends Service
|
||||
|
||||
// Project
|
||||
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
|
||||
$this->addAction(UpdateProjectProtocolStatus::getName(), new UpdateProjectProtocolStatus());
|
||||
$this->addAction(UpdateProjectServiceStatus::getName(), new UpdateProjectServiceStatus());
|
||||
$this->addAction(UpdateProjectProtocol::getName(), new UpdateProjectProtocol());
|
||||
$this->addAction(UpdateProjectService::getName(), new UpdateProjectService());
|
||||
|
||||
// SMTP
|
||||
$this->addAction(UpdateSMTP::getName(), new UpdateSMTP());
|
||||
$this->addAction(CreateSMTPTest::getName(), new CreateSMTPTest());
|
||||
|
||||
// Templates
|
||||
$this->addAction(GetTemplate::getName(), new GetTemplate());
|
||||
$this->addAction(UpdateTemplate::getName(), new UpdateTemplate());
|
||||
|
||||
// Variables
|
||||
$this->addAction(CreateVariable::getName(), new CreateVariable());
|
||||
|
||||
@@ -21,8 +21,6 @@ use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
@@ -209,32 +207,16 @@ class Create extends Action
|
||||
}
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
|
||||
$projectTables = !\in_array($dsn->getHost(), $sharedTables);
|
||||
$sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1);
|
||||
$sharedTablesV2 = !$projectTables && !$sharedTablesV1;
|
||||
$sharedTables = $sharedTablesV1 || $sharedTablesV2;
|
||||
|
||||
if (!$sharedTablesV2) {
|
||||
if ($projectTables) {
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$dbForProject = new Database($adapter, $cache);
|
||||
$dbForProject->setDatabase(APP_DATABASE);
|
||||
|
||||
if ($sharedTables) {
|
||||
$tenant = null;
|
||||
if ($sharedTablesV1) {
|
||||
$tenant = $project->getSequence();
|
||||
}
|
||||
$dbForProject
|
||||
->setSharedTables(true)
|
||||
->setTenant($tenant)
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$dbForProject
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
$dbForProject
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
|
||||
$create = true;
|
||||
|
||||
@@ -244,27 +226,11 @@ class Create extends Action
|
||||
$create = false;
|
||||
}
|
||||
|
||||
if ($create || $projectTables) {
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
$audit = new Audit($adapter);
|
||||
$audit->setup();
|
||||
}
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
$audit = new Audit($adapter);
|
||||
$audit->setup();
|
||||
|
||||
if (!$create && $sharedTablesV1) {
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
$attributes = $adapter->getAttributeDocuments();
|
||||
$indexes = $adapter->getIndexDocuments();
|
||||
$dbForProject->createDocument(Database::METADATA, new Document([
|
||||
'$id' => ID::custom('audit'),
|
||||
'$permissions' => [Permission::create(Role::any())],
|
||||
'name' => 'audit',
|
||||
'attributes' => $attributes,
|
||||
'indexes' => $indexes,
|
||||
'documentSecurity' => true
|
||||
]));
|
||||
}
|
||||
|
||||
if ($create || $sharedTablesV1) {
|
||||
if ($create) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', [])['projects'] ?? [];
|
||||
|
||||
@@ -279,37 +245,7 @@ class Create extends Action
|
||||
try {
|
||||
$dbForProject->createCollection($key, $attributes, $indexes);
|
||||
} catch (Duplicate) {
|
||||
try {
|
||||
$dbForProject->createDocument(Database::METADATA, new Document([
|
||||
'$id' => ID::custom($key),
|
||||
'$permissions' => [Permission::create(Role::any())],
|
||||
'name' => $key,
|
||||
'attributes' => $attributes,
|
||||
'indexes' => $indexes,
|
||||
'documentSecurity' => true
|
||||
]));
|
||||
} catch (Duplicate) {
|
||||
// Metadata already exists from concurrent creation
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// PostgreSQL adapter may throw a non-Duplicate exception when
|
||||
// a table or index already exists during concurrent project
|
||||
// creation in shared mode. Treat as duplicate if metadata
|
||||
// can be created successfully.
|
||||
try {
|
||||
$dbForProject->createDocument(Database::METADATA, new Document([
|
||||
'$id' => ID::custom($key),
|
||||
'$permissions' => [Permission::create(Role::any())],
|
||||
'name' => $key,
|
||||
'attributes' => $attributes,
|
||||
'indexes' => $indexes,
|
||||
'documentSecurity' => true
|
||||
]));
|
||||
} catch (Duplicate) {
|
||||
// Metadata already exists from concurrent creation
|
||||
} catch (\Throwable) {
|
||||
throw $e; // Rethrow original if metadata creation also fails
|
||||
}
|
||||
// Collection already exists
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\API;
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Proxy\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -62,7 +62,7 @@ class Create extends Action
|
||||
->param('domain', null, new ValidatorDomain(), 'Domain name.')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('platform')
|
||||
@@ -70,7 +70,7 @@ class Create extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log)
|
||||
public function action(string $domain, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log)
|
||||
{
|
||||
$this->validateDomainRestrictions($domain, $platform);
|
||||
|
||||
@@ -114,13 +114,14 @@ class Create extends Action
|
||||
}
|
||||
|
||||
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
|
||||
$queueForCertificates
|
||||
->setDomain(new Document([
|
||||
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: $project,
|
||||
domain: new Document([
|
||||
'domain' => $rule->getAttribute('domain'),
|
||||
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
|
||||
]))
|
||||
->setAction(Certificate::ACTION_GENERATION)
|
||||
->trigger();
|
||||
]),
|
||||
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
|
||||
));
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('ruleId', $rule->getId());
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Function;
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Proxy\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -66,7 +66,7 @@ class Create extends Action
|
||||
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('dbForProject')
|
||||
@@ -75,7 +75,7 @@ class Create extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
|
||||
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
|
||||
{
|
||||
$this->validateDomainRestrictions($domain, $platform);
|
||||
|
||||
@@ -132,13 +132,14 @@ class Create extends Action
|
||||
}
|
||||
|
||||
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
|
||||
$queueForCertificates
|
||||
->setDomain(new Document([
|
||||
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: $project,
|
||||
domain: new Document([
|
||||
'domain' => $rule->getAttribute('domain'),
|
||||
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
|
||||
]))
|
||||
->setAction(Certificate::ACTION_GENERATION)
|
||||
->trigger();
|
||||
]),
|
||||
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
|
||||
));
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('ruleId', $rule->getId());
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect;
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Proxy\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -69,7 +69,7 @@ class Create extends Action
|
||||
->param('resourceType', '', new WhiteList(['site', 'function']), 'Type of parent resource.')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('dbForProject')
|
||||
@@ -78,7 +78,7 @@ class Create extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
|
||||
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
|
||||
{
|
||||
$this->validateDomainRestrictions($domain, $platform);
|
||||
|
||||
@@ -136,13 +136,14 @@ class Create extends Action
|
||||
}
|
||||
|
||||
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
|
||||
$queueForCertificates
|
||||
->setDomain(new Document([
|
||||
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: $project,
|
||||
domain: new Document([
|
||||
'domain' => $rule->getAttribute('domain'),
|
||||
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
|
||||
]))
|
||||
->setAction(Certificate::ACTION_GENERATION)
|
||||
->trigger();
|
||||
]),
|
||||
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
|
||||
));
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('ruleId', $rule->getId());
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Site;
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Proxy\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -43,6 +43,13 @@ class Create extends Action
|
||||
->label('event', 'rules.[ruleId].create')
|
||||
->label('audits.event', 'rule.create')
|
||||
->label('audits.resource', 'rule/{response.$id}')
|
||||
->label('audits.payload', true)
|
||||
->label('audits.payload.allowList', [
|
||||
'domain', 'type', 'region', 'trigger', // Generic config
|
||||
'deploymentId', 'deploymentInternalId', // Deployment congif
|
||||
'deploymentResourceInternalId', 'deploymentResourceId', 'deploymentResourceType', // Site / Function config
|
||||
'redirectUrl', 'redirectStatusCode', // Redirect config
|
||||
])
|
||||
->label('sdk', new Method(
|
||||
namespace: 'proxy',
|
||||
group: null,
|
||||
@@ -66,7 +73,7 @@ class Create extends Action
|
||||
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('dbForProject')
|
||||
@@ -75,7 +82,7 @@ class Create extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
|
||||
public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
|
||||
{
|
||||
$this->validateDomainRestrictions($domain, $platform);
|
||||
|
||||
@@ -132,13 +139,14 @@ class Create extends Action
|
||||
}
|
||||
|
||||
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
|
||||
$queueForCertificates
|
||||
->setDomain(new Document([
|
||||
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: $project,
|
||||
domain: new Document([
|
||||
'domain' => $rule->getAttribute('domain'),
|
||||
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
|
||||
]))
|
||||
->setAction(Certificate::ACTION_GENERATION)
|
||||
->trigger();
|
||||
]),
|
||||
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
|
||||
));
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('ruleId', $rule->getId());
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Verification;
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Proxy\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -56,7 +56,7 @@ class Update extends Action
|
||||
))
|
||||
->param('ruleId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Rule ID.', false, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
@@ -67,7 +67,7 @@ class Update extends Action
|
||||
public function action(
|
||||
string $ruleId,
|
||||
Response $response,
|
||||
Certificate $queueForCertificates,
|
||||
Certificate $publisherForCertificates,
|
||||
Event $queueForEvents,
|
||||
Document $project,
|
||||
Database $dbForPlatform,
|
||||
@@ -110,12 +110,13 @@ class Update extends Action
|
||||
}
|
||||
|
||||
// Issue a TLS certificate when DNS verification is successful
|
||||
$queueForCertificates
|
||||
->setDomain(new Document([
|
||||
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: $project,
|
||||
domain: new Document([
|
||||
'domain' => $rule->getAttribute('domain'),
|
||||
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
|
||||
]))
|
||||
->trigger();
|
||||
]),
|
||||
));
|
||||
|
||||
if (!empty($certificate)) {
|
||||
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
|
||||
|
||||
@@ -54,7 +54,7 @@ class Get extends Action
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'bucket/{request.bucketId}')
|
||||
->label('cache.resource', 'file/{request.fileId}')
|
||||
->label('cache.params', ['width', 'height', 'gravity', 'quality', 'borderWidth', 'borderColor', 'borderRadius', 'opacity', 'rotation', 'background', 'output'])
|
||||
->label('cache.params', ['width', 'height', 'gravity', 'quality', 'borderWidth', 'borderColor', 'borderRadius', 'opacity', 'rotation', 'background', 'output', 'project'])
|
||||
->label('sdk', new Method(
|
||||
namespace: 'storage',
|
||||
group: 'files',
|
||||
|
||||
@@ -324,7 +324,9 @@ class Create extends Action
|
||||
$body = $locale->getText('emails.invitation.body');
|
||||
$preview = $locale->getText('emails.invitation.preview');
|
||||
$subject = $locale->getText('emails.invitation.subject');
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.invitation-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.invitation-' . $locale->fallback] ?? [];
|
||||
|
||||
$message = Template::fromFile(APP_CE_CONFIG_DIR . '/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
@@ -341,7 +343,8 @@ class Create extends Action
|
||||
|
||||
$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'])) {
|
||||
@@ -350,8 +353,13 @@ class Create extends Action
|
||||
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
|
||||
@@ -368,8 +376,13 @@ class Create extends Action
|
||||
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'] ?? '';
|
||||
@@ -377,7 +390,8 @@ class Create extends Action
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
@@ -407,11 +421,6 @@ class Create extends Action
|
||||
|
||||
$message = Template::fromFile(APP_CE_CONFIG_DIR . '/locale/templates/sms-base.tpl');
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? [];
|
||||
if (! empty($customTemplate)) {
|
||||
$message = $customTemplate['message'];
|
||||
}
|
||||
|
||||
$message = $message->setParam('{{token}}', $url);
|
||||
$message = $message->render();
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ class Delete extends Action
|
||||
if ($team->getAttribute('userInternalId') === $membership->getAttribute('userInternalId')) {
|
||||
$membership = $dbForProject->findOne('memberships', [
|
||||
Query::equal('teamInternalId', [$team->getSequence()]),
|
||||
Query::equal('confirm', [true]),
|
||||
]);
|
||||
|
||||
if (!$membership->isEmpty()) {
|
||||
|
||||
+1
@@ -307,6 +307,7 @@ class Create extends Action
|
||||
];
|
||||
}
|
||||
|
||||
$output->setAttribute('type', $type);
|
||||
$output->setAttribute('variables', $variables);
|
||||
|
||||
$response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME);
|
||||
|
||||
@@ -313,6 +313,7 @@ class XList extends Action
|
||||
}, $repos);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'type' => $type,
|
||||
$type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos,
|
||||
'total' => $total,
|
||||
]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use DateTime;
|
||||
use Swoole\Coroutine\Channel;
|
||||
use Swoole\Process;
|
||||
@@ -29,16 +29,16 @@ class Interval extends Action
|
||||
->desc('Schedules tasks on regular intervals by publishing them to our queues')
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates): void
|
||||
public function action(Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates): void
|
||||
{
|
||||
Console::title('Interval V1');
|
||||
Console::success(APP_NAME . ' interval process v1 has started');
|
||||
|
||||
$timers = $this->runTasks($dbForPlatform, $getProjectDB, $queueForCertificates);
|
||||
$timers = $this->runTasks($dbForPlatform, $getProjectDB, $publisherForCertificates);
|
||||
|
||||
$chan = new Channel(1);
|
||||
Process::signal(SIGTERM, function () use ($chan) {
|
||||
@@ -52,16 +52,16 @@ class Interval extends Action
|
||||
}
|
||||
}
|
||||
|
||||
public function runTasks(Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates): array
|
||||
public function runTasks(Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates): array
|
||||
{
|
||||
$timers = [];
|
||||
$tasks = $this->getTasks();
|
||||
foreach ($tasks as $task) {
|
||||
$timers[] = Timer::tick($task['interval'], function () use ($task, $dbForPlatform, $getProjectDB, $queueForCertificates) {
|
||||
$timers[] = Timer::tick($task['interval'], function () use ($task, $dbForPlatform, $getProjectDB, $publisherForCertificates) {
|
||||
$taskName = $task['name'];
|
||||
Span::init("interval.{$taskName}");
|
||||
try {
|
||||
$task['callback']($dbForPlatform, $getProjectDB, $queueForCertificates);
|
||||
$task['callback']($dbForPlatform, $getProjectDB, $publisherForCertificates);
|
||||
} catch (\Exception $e) {
|
||||
Span::error($e);
|
||||
} finally {
|
||||
@@ -80,15 +80,15 @@ class Interval extends Action
|
||||
return [
|
||||
[
|
||||
'name' => 'domainVerification',
|
||||
"callback" => function (Database $dbForPlatform, callable $getProjectDB, Certificate $queueForCertificates) {
|
||||
$this->verifyDomain($dbForPlatform, $queueForCertificates);
|
||||
"callback" => function (Database $dbForPlatform, callable $getProjectDB, Certificate $publisherForCertificates) {
|
||||
$this->verifyDomain($dbForPlatform, $publisherForCertificates);
|
||||
},
|
||||
'interval' => $intervalDomainVerification * 1000,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function verifyDomain(Database $dbForPlatform, Certificate $queueForCertificates): void
|
||||
private function verifyDomain(Database $dbForPlatform, Certificate $publisherForCertificates): void
|
||||
{
|
||||
$time = DatabaseDateTime::now();
|
||||
$fromTime = new DateTime('-3 days'); // Max 3 days old
|
||||
@@ -115,13 +115,17 @@ class Interval extends Action
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
try {
|
||||
$queueForCertificates
|
||||
->setDomain(new Document([
|
||||
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: new Document([
|
||||
'$id' => $rule->getAttribute('projectId', ''),
|
||||
'$sequence' => $rule->getAttribute('projectInternalId', 0),
|
||||
]),
|
||||
domain: new Document([
|
||||
'domain' => $rule->getAttribute('domain'),
|
||||
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
|
||||
]))
|
||||
->setAction(Certificate::ACTION_DOMAIN_VERIFICATION)
|
||||
->trigger();
|
||||
]),
|
||||
action: \Appwrite\Event\Certificate::ACTION_DOMAIN_VERIFICATION,
|
||||
));
|
||||
$processed++;
|
||||
} catch (\Throwable $th) {
|
||||
$failed++;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use Utopia\Console;
|
||||
@@ -29,12 +29,12 @@ class Maintenance extends Action
|
||||
->param('type', 'loop', new WhiteList(['loop', 'trigger']), 'How to run task. "loop" is meant for container entrypoint, and "trigger" for manual execution.')
|
||||
->inject('dbForPlatform')
|
||||
->inject('console')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('queueForDeletes')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $type, Database $dbForPlatform, Document $console, Certificate $queueForCertificates, Delete $queueForDeletes): void
|
||||
public function action(string $type, Database $dbForPlatform, Document $console, Certificate $publisherForCertificates, Delete $queueForDeletes): void
|
||||
{
|
||||
Console::title('Maintenance V1');
|
||||
Console::success(APP_NAME . ' maintenance process v1 has started');
|
||||
@@ -59,7 +59,7 @@ class Maintenance extends Action
|
||||
$delay = $next->getTimestamp() - $now->getTimestamp();
|
||||
}
|
||||
|
||||
$action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $queueForCertificates) {
|
||||
$action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $publisherForCertificates) {
|
||||
$time = DatabaseDateTime::now();
|
||||
|
||||
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
|
||||
@@ -92,7 +92,7 @@ class Maintenance extends Action
|
||||
->trigger();
|
||||
|
||||
$this->notifyDeleteConnections($queueForDeletes);
|
||||
$this->renewCertificates($dbForPlatform, $queueForCertificates);
|
||||
$this->renewCertificates($dbForPlatform, $publisherForCertificates);
|
||||
$this->notifyDeleteCache($cacheRetention, $queueForDeletes);
|
||||
$this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes);
|
||||
$this->notifyDeleteCSVExports($queueForDeletes);
|
||||
@@ -124,7 +124,7 @@ class Maintenance extends Action
|
||||
->trigger();
|
||||
}
|
||||
|
||||
private function renewCertificates(Database $dbForPlatform, Certificate $queueForCertificate): void
|
||||
private function renewCertificates(Database $dbForPlatform, Certificate $publisherForCertificate): void
|
||||
{
|
||||
$time = DatabaseDateTime::now();
|
||||
|
||||
@@ -158,13 +158,17 @@ class Maintenance extends Action
|
||||
continue;
|
||||
}
|
||||
|
||||
$queueForCertificate
|
||||
->setDomain(new Document([
|
||||
$publisherForCertificate->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: new Document([
|
||||
'$id' => $rule->getAttribute('projectId', ''),
|
||||
'$sequence' => $rule->getAttribute('projectInternalId', 0),
|
||||
]),
|
||||
domain: new Document([
|
||||
'domain' => $rule->getAttribute('domain'),
|
||||
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
|
||||
]))
|
||||
->setAction(Certificate::ACTION_GENERATION)
|
||||
->trigger();
|
||||
]),
|
||||
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Tasks;
|
||||
use Appwrite\SDK\Language\AgentSkills;
|
||||
use Appwrite\SDK\Language\Android;
|
||||
use Appwrite\SDK\Language\Apple;
|
||||
use Appwrite\SDK\Language\ClaudePlugin;
|
||||
use Appwrite\SDK\Language\CLI;
|
||||
use Appwrite\SDK\Language\CursorPlugin;
|
||||
use Appwrite\SDK\Language\Dart;
|
||||
@@ -451,6 +452,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
case 'cursor-plugin':
|
||||
$config = new CursorPlugin();
|
||||
break;
|
||||
case 'claude-plugin':
|
||||
$config = new ClaudePlugin();
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('Language "' . $language['key'] . '" not supported');
|
||||
}
|
||||
@@ -622,29 +626,28 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
$repo->execute('config', 'advice.defaultBranchName', 'false');
|
||||
$repo->addRemote('origin', $gitUrl);
|
||||
|
||||
// Fetch and checkout base branch (or create if new repo)
|
||||
// Fetch and checkout the target branch (e.g. dev) if it exists on remote,
|
||||
// otherwise create it from the base branch (e.g. main).
|
||||
// We build on top of the existing remote branch so a regular push
|
||||
// works without force-pushing against protected branches.
|
||||
$hasBranch = false;
|
||||
try {
|
||||
$repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch);
|
||||
$repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $gitBranch);
|
||||
$hasBranch = true;
|
||||
} catch (\Throwable) {
|
||||
// Branch doesn't exist on remote yet
|
||||
}
|
||||
|
||||
if ($hasBranch) {
|
||||
$repo->execute('checkout', '-f', $gitBranch);
|
||||
} else {
|
||||
// Fetch base branch to create the target branch from it
|
||||
try {
|
||||
$repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch);
|
||||
$repo->execute('checkout', '-f', $repoBranch);
|
||||
} catch (\Throwable) {
|
||||
$repo->execute('checkout', '-b', $repoBranch);
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$repo->execute('checkout', '-b', $repoBranch);
|
||||
}
|
||||
|
||||
try {
|
||||
$repo->execute('pull', 'origin', $repoBranch, '--quiet', '--no-tags');
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
// Create or checkout dev branch from the base branch
|
||||
// This ensures dev always starts from the latest base branch,
|
||||
// avoiding history divergence caused by squash merges.
|
||||
try {
|
||||
$repo->execute('checkout', '-B', $gitBranch, $repoBranch);
|
||||
} catch (\Throwable) {
|
||||
$repo->execute('checkout', '-b', $gitBranch);
|
||||
}
|
||||
|
||||
@@ -685,7 +688,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
return true;
|
||||
}
|
||||
|
||||
$repo->execute('push', '--force-with-lease', '-u', 'origin', $gitBranch, '--quiet');
|
||||
$repo->execute('push', '-u', 'origin', $gitBranch, '--quiet');
|
||||
} catch (\Throwable $e) {
|
||||
Console::warning(" Git push failed: " . $e->getMessage());
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -29,11 +29,11 @@ class SSL extends Action
|
||||
->param('skip-check', 'true', new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true)
|
||||
->inject('console')
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $queueForCertificates): void
|
||||
public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates): void
|
||||
{
|
||||
$domain = new Domain(!empty($domain) ? $domain : '');
|
||||
if (!$domain->isKnown() || $domain->isTest()) {
|
||||
@@ -98,12 +98,13 @@ class SSL extends Action
|
||||
Console::info('Updated existing rule ' . $rule->getId() . ' for domain: ' . $domain->get());
|
||||
}
|
||||
|
||||
$queueForCertificates
|
||||
->setDomain(new Document([
|
||||
'domain' => $domain->get()
|
||||
]))
|
||||
->setSkipRenewCheck($skipCheck)
|
||||
->trigger();
|
||||
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: $console,
|
||||
domain: new Document([
|
||||
'domain' => $domain->get(),
|
||||
]),
|
||||
skipRenewCheck: $skipCheck,
|
||||
));
|
||||
|
||||
Console::success('Scheduled a job to issue a TLS certificate for domain: ' . $domain->get());
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use Cron\CronExpression;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
|
||||
/**
|
||||
* ScheduleFunctions
|
||||
@@ -88,7 +90,7 @@ class ScheduleFunctions extends ScheduleBase
|
||||
$scheduleKey = $delayConfig['key'];
|
||||
// Ensure schedule was not deleted
|
||||
if (!\array_key_exists($scheduleKey, $this->schedules)) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
$schedule = $this->schedules[$scheduleKey];
|
||||
@@ -102,8 +104,22 @@ class ScheduleFunctions extends ScheduleBase
|
||||
->setFunction($schedule['resource'])
|
||||
->setMethod('POST')
|
||||
->setPath('/')
|
||||
->setProject($schedule['project'])
|
||||
->trigger();
|
||||
->setProject($schedule['project']);
|
||||
|
||||
$projectDoc = $schedule['project'];
|
||||
$functionDoc = $schedule['resource'];
|
||||
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
|
||||
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
|
||||
if ($traceProjectId !== '' && $traceFunctionId !== '' && $projectDoc->getId() === $traceProjectId && $functionDoc->getId() === $traceFunctionId) {
|
||||
Span::init('execution.trace.v1_functions_enqueue');
|
||||
Span::add('datetime', gmdate('c'));
|
||||
Span::add('projectId', $projectDoc->getId());
|
||||
Span::add('functionId', $functionDoc->getId());
|
||||
Span::add('scheduleId', $schedule['$id'] ?? '');
|
||||
Span::current()?->finish();
|
||||
}
|
||||
|
||||
$queueForFunctions->trigger();
|
||||
|
||||
$this->recordEnqueueDelay($delayConfig['nextDate']);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Event\Message\Audit;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use Utopia\Console;
|
||||
@@ -40,7 +41,6 @@ class Audits extends Action
|
||||
$this
|
||||
->desc('Audits worker')
|
||||
->inject('message')
|
||||
->inject('project')
|
||||
->inject('getAudit')
|
||||
->callback($this->action(...));
|
||||
|
||||
@@ -50,14 +50,13 @@ class Audits extends Action
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @param Document $project
|
||||
* @param callable(Document): \Utopia\Audit\Audit $getAudit
|
||||
* @return Commit|NoCommit
|
||||
* @throws Throwable
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws Structure
|
||||
*/
|
||||
public function action(Message $message, Document $project, callable $getAudit): Commit|NoCommit
|
||||
public function action(Message $message, callable $getAudit): Commit|NoCommit
|
||||
{
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
@@ -65,19 +64,21 @@ class Audits extends Action
|
||||
throw new Exception('Missing payload');
|
||||
}
|
||||
|
||||
$auditMessage = Audit::fromArray($payload);
|
||||
|
||||
Console::info('Aggregating audit logs');
|
||||
|
||||
$event = $payload['event'] ?? '';
|
||||
$event = $auditMessage->event;
|
||||
|
||||
$auditPayload = '';
|
||||
if ($project->getId() === 'console') {
|
||||
$auditPayload = $payload['payload'] ?? '';
|
||||
if ($auditMessage->project->getId() === 'console') {
|
||||
$auditPayload = $auditMessage->payload;
|
||||
}
|
||||
$mode = $payload['mode'] ?? '';
|
||||
$resource = $payload['resource'] ?? '';
|
||||
$userAgent = $payload['userAgent'] ?? '';
|
||||
$ip = $payload['ip'] ?? '';
|
||||
$user = new Document($payload['user'] ?? []);
|
||||
$mode = $auditMessage->mode;
|
||||
$resource = $auditMessage->resource;
|
||||
$userAgent = $auditMessage->userAgent;
|
||||
$ip = $auditMessage->ip;
|
||||
$user = $auditMessage->user;
|
||||
|
||||
$impersonatorUserId = $user->getAttribute('impersonatorUserId');
|
||||
$actorUserId = $impersonatorUserId ?: $user->getId();
|
||||
@@ -126,14 +127,14 @@ class Audits extends Action
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($this->logs[$project->getSequence()])) {
|
||||
$this->logs[$project->getSequence()]['logs'][] = $eventData;
|
||||
if (isset($this->logs[$auditMessage->project->getSequence()])) {
|
||||
$this->logs[$auditMessage->project->getSequence()]['logs'][] = $eventData;
|
||||
} else {
|
||||
$this->logs[$project->getSequence()] = [
|
||||
$this->logs[$auditMessage->project->getSequence()] = [
|
||||
'project' => new Document([
|
||||
'$id' => $project->getId(),
|
||||
'$sequence' => $project->getSequence(),
|
||||
'database' => $project->getAttribute('database'),
|
||||
'$id' => $auditMessage->project->getId(),
|
||||
'$sequence' => $auditMessage->project->getSequence(),
|
||||
'database' => $auditMessage->project->getAttribute('database'),
|
||||
]),
|
||||
'logs' => [$eventData]
|
||||
];
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Certificates\Adapter as CertificatesAdapter;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
@@ -55,7 +55,7 @@ class Certificates extends Action
|
||||
->inject('queueForWebhooks')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('log')
|
||||
->inject('certificates')
|
||||
->inject('plan')
|
||||
@@ -71,7 +71,7 @@ class Certificates extends Action
|
||||
* @param Webhook $queueForWebhooks
|
||||
* @param Func $queueForFunctions
|
||||
* @param Realtime $queueForRealtime
|
||||
* @param Certificate $queueForCertificates
|
||||
* @param Certificate $publisherForCertificates
|
||||
* @param Log $log
|
||||
* @param CertificatesAdapter $certificates
|
||||
* @param array $plan
|
||||
@@ -88,7 +88,7 @@ class Certificates extends Action
|
||||
Webhook $queueForWebhooks,
|
||||
Func $queueForFunctions,
|
||||
Realtime $queueForRealtime,
|
||||
Certificate $queueForCertificates,
|
||||
Certificate $publisherForCertificates,
|
||||
Log $log,
|
||||
CertificatesAdapter $certificates,
|
||||
array $plan,
|
||||
@@ -100,21 +100,22 @@ class Certificates extends Action
|
||||
throw new Exception('Missing payload');
|
||||
}
|
||||
|
||||
$document = new Document($payload['domain'] ?? []);
|
||||
$certificateMessage = \Appwrite\Event\Message\Certificate::fromArray($payload);
|
||||
$document = $certificateMessage->domain;
|
||||
$domain = new Domain($document->getAttribute('domain', ''));
|
||||
$domainType = $document->getAttribute('domainType');
|
||||
$skipRenewCheck = $payload['skipRenewCheck'] ?? false;
|
||||
$validationDomain = $payload['validationDomain'] ?? null;
|
||||
$action = $payload['action'] ?? Certificate::ACTION_GENERATION;
|
||||
$skipRenewCheck = $certificateMessage->skipRenewCheck;
|
||||
$validationDomain = $certificateMessage->validationDomain;
|
||||
$action = $certificateMessage->action;
|
||||
|
||||
$log->addTag('domain', $domain->get());
|
||||
|
||||
switch ($action) {
|
||||
case Certificate::ACTION_DOMAIN_VERIFICATION:
|
||||
$this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForCertificates, $log, $authorization, $validationDomain);
|
||||
case \Appwrite\Event\Certificate::ACTION_DOMAIN_VERIFICATION:
|
||||
$this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $publisherForCertificates, $log, $authorization, $validationDomain);
|
||||
break;
|
||||
|
||||
case Certificate::ACTION_GENERATION:
|
||||
case \Appwrite\Event\Certificate::ACTION_GENERATION:
|
||||
$this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain);
|
||||
break;
|
||||
|
||||
@@ -130,7 +131,7 @@ class Certificates extends Action
|
||||
* @param Webhook $queueForWebhooks
|
||||
* @param Func $queueForFunctions
|
||||
* @param Realtime $queueForRealtime
|
||||
* @param Certificate $queueForCertificates
|
||||
* @param Certificate $publisherForCertificates
|
||||
* @param Log $log
|
||||
* @param ValidatorAuthorization $authorization
|
||||
* @param string|null $validationDomain
|
||||
@@ -146,7 +147,7 @@ class Certificates extends Action
|
||||
Webhook $queueForWebhooks,
|
||||
Func $queueForFunctions,
|
||||
Realtime $queueForRealtime,
|
||||
Certificate $queueForCertificates,
|
||||
Certificate $publisherForCertificates,
|
||||
Log $log,
|
||||
ValidatorAuthorization $authorization,
|
||||
?string $validationDomain = null
|
||||
@@ -188,13 +189,17 @@ class Certificates extends Action
|
||||
|
||||
// Issue a TLS certificate when domain is verified
|
||||
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
|
||||
$queueForCertificates
|
||||
->setDomain(new Document([
|
||||
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: new Document([
|
||||
'$id' => $rule->getAttribute('projectId', ''),
|
||||
'$sequence' => $rule->getAttribute('projectInternalId', 0),
|
||||
]),
|
||||
domain: new Document([
|
||||
'domain' => $rule->getAttribute('domain'),
|
||||
'domainType' => $rule->getAttribute('deploymentResourceType', $rule->getAttribute('type')),
|
||||
]))
|
||||
->setAction(Certificate::ACTION_GENERATION)
|
||||
->trigger();
|
||||
]),
|
||||
action: \Appwrite\Event\Certificate::ACTION_GENERATION,
|
||||
));
|
||||
|
||||
Console::success('Certificate generation triggered successfully.');
|
||||
}
|
||||
|
||||
@@ -651,11 +651,8 @@ class Deletes extends Action
|
||||
];
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
|
||||
|
||||
$projectTables = !\in_array($dsn->getHost(), $sharedTables);
|
||||
$sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1);
|
||||
$sharedTablesV2 = !$projectTables && !$sharedTablesV1;
|
||||
|
||||
$allDatabases = [
|
||||
new Document([
|
||||
@@ -758,15 +755,7 @@ class Deletes extends Action
|
||||
),
|
||||
$databasesToClean
|
||||
));
|
||||
} elseif ($sharedTablesV1) {
|
||||
$this->deleteByGroup(
|
||||
Database::METADATA,
|
||||
[
|
||||
Query::orderAsc()
|
||||
],
|
||||
$dbForProject
|
||||
);
|
||||
} elseif ($sharedTablesV2) {
|
||||
} else {
|
||||
$queries = \array_map(
|
||||
fn ($id) => Query::notEqual('$id', $id),
|
||||
$projectCollectionIds
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user