Merge branch '1.8.x' into ser-331

This commit is contained in:
Hemachandar
2025-12-23 16:37:22 +05:30
committed by GitHub
47 changed files with 2184 additions and 1301 deletions
+2 -2
View File
@@ -69,8 +69,8 @@ _APP_STORAGE_ANTIVIRUS_PORT=3310
_APP_SMTP_HOST=maildev
_APP_SMTP_PORT=1025
_APP_SMTP_SECURE=
_APP_SMTP_USERNAME=
_APP_SMTP_PASSWORD=
_APP_SMTP_USERNAME=user
_APP_SMTP_PASSWORD=password
_APP_SMS_PROVIDER=sms://username:password@mock
_APP_SMS_FROM=+123456789
_APP_SMS_PROJECTS_DENY_LIST=
+73
View File
@@ -0,0 +1,73 @@
# AGENTS.md
Appwrite is an end-to-end backend server for web, mobile, native, and backend apps. This guide provides context and instructions for AI coding agents working on the Appwrite codebase.
## Project Overview
Appwrite is a self-hosted Backend-as-a-Service (BaaS) platform that provides developers with a set of APIs and tools to build secure, scalable applications. The project uses a hybrid monolithic-microservice architecture built with PHP, running on Swoole for high performance.
**Key Technologies:**
- **Backend:** PHP 8.3+, Swoole
- **Libraries:** Utopia PHP
- **Database:** MariaDB, Redis
- **Cache:** Redis
- **Queue:** Redis
- **Containers:** Docker
## Development Commands
```bash
# Run Appwrite
docker compose up -d --force-recreate --build
# Run specific test
docker compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName] --filter=[FunctionName]
# Format code
composer format
```
## Code Style Guidelines
- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standard
- Use PSR-4 autoloading
- Strict type declarations where applicable
- Comprehensive PHPDoc comments
### Naming Conventions
#### `resourceType` Naming Rule
When a collection has a combination of `resourceType`, `resourceId`, and/or `resourceInternalId`, the value of `resourceType` MUST always be **plural** - for example: `functions`, `sites`, `deployments`.
Examples:
```php
'resourceType' => 'functions'
'resourceType' => 'sites'
'resourceType' => 'deployments'
```
## Security Considerations
### Critical Security Practices
- **Never hardcode credentials** - Use environment variables
- **Rate limiting** - Respect abuse prevention mechanisms
## Dependencies
Avoid introducing new dependencies other than utopia-php.
## Pull Request Guidelines
### Before Submitting
- Run `composer format`
- Update documentation if adding features
- Add/update tests for your changes
- Check that Docker build succeeds
`docs/specs/authentication.drawio.svg`
## Known Issues and Gotchas
- **Hot Reload:** Code changes require container restart in some cases
- **Logging:** There is no central place for logs, so when debugging, ensure to check all possibly relevant containers
+11
View File
@@ -462,4 +462,15 @@ return [
'mock' => true,
'class' => 'Appwrite\\Auth\\OAuth2\\Mock',
],
'mock-unverified' => [
'name' => 'MockUnverified',
'developers' => 'https://appwrite.io',
'icon' => 'icon-appwrite',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => true,
'class' => 'Appwrite\\Auth\\OAuth2\\MockUnverified',
],
];
+2
View File
@@ -58,6 +58,8 @@ $admins = [
'projects.write',
'keys.read',
'keys.write',
'devKeys.read',
'devKeys.write',
'webhooks.read',
'webhooks.write',
'locale.read',
+9 -9
View File
@@ -262,7 +262,7 @@ return [
[
'key' => 'nodejs',
'name' => 'Node.js',
'version' => '21.0.0',
'version' => '21.1.0',
'url' => 'https://github.com/appwrite/sdk-for-node',
'package' => 'https://www.npmjs.com/package/node-appwrite',
'enabled' => true,
@@ -281,7 +281,7 @@ return [
[
'key' => 'php',
'name' => 'PHP',
'version' => '19.0.0',
'version' => '19.1.0',
'url' => 'https://github.com/appwrite/sdk-for-php',
'package' => 'https://packagist.org/packages/appwrite/appwrite',
'enabled' => true,
@@ -300,7 +300,7 @@ return [
[
'key' => 'python',
'name' => 'Python',
'version' => '14.0.0',
'version' => '14.1.0',
'url' => 'https://github.com/appwrite/sdk-for-python',
'package' => 'https://pypi.org/project/appwrite/',
'enabled' => true,
@@ -319,7 +319,7 @@ return [
[
'key' => 'ruby',
'name' => 'Ruby',
'version' => '20.0.0',
'version' => '20.1.0',
'url' => 'https://github.com/appwrite/sdk-for-ruby',
'package' => 'https://rubygems.org/gems/appwrite',
'enabled' => true,
@@ -338,7 +338,7 @@ return [
[
'key' => 'go',
'name' => 'Go',
'version' => 'v0.15.0',
'version' => 'v0.16.0',
'url' => 'https://github.com/appwrite/sdk-for-go',
'package' => 'https://github.com/appwrite/sdk-for-go',
'enabled' => true,
@@ -357,7 +357,7 @@ return [
[
'key' => 'dotnet',
'name' => '.NET',
'version' => '0.23.0',
'version' => '0.24.0',
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
'package' => 'https://www.nuget.org/packages/Appwrite',
'enabled' => true,
@@ -376,7 +376,7 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
'version' => '20.0.0',
'version' => '20.1.0',
'url' => 'https://github.com/appwrite/sdk-for-dart',
'package' => 'https://pub.dev/packages/dart_appwrite',
'enabled' => true,
@@ -395,7 +395,7 @@ return [
[
'key' => 'kotlin',
'name' => 'Kotlin',
'version' => '13.0.0',
'version' => '13.1.0',
'url' => 'https://github.com/appwrite/sdk-for-kotlin',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-kotlin',
'enabled' => true,
@@ -418,7 +418,7 @@ return [
[
'key' => 'swift',
'name' => 'Swift',
'version' => '14.0.0',
'version' => '14.1.0',
'url' => 'https://github.com/appwrite/sdk-for-swift',
'package' => 'https://github.com/appwrite/sdk-for-swift',
'enabled' => true,
@@ -7280,7 +7280,7 @@
},
"attributes": {
"type": "array",
"description": "Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime, relationship), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.",
"description": "Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.",
"x-example": null,
"items": {
"type": "object"
+1 -1
View File
@@ -6754,7 +6754,7 @@
},
"attributes": {
"type": "array",
"description": "Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime, relationship), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.",
"description": "Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.",
"x-example": null,
"items": {
"type": "object"
+66 -48
View File
@@ -429,7 +429,23 @@
"Session": [],
"JWT": []
}
]
],
"requestBody": {
"content": {
"application\/json": {
"schema": {
"type": "object",
"properties": {
"duration": {
"type": "integer",
"description": "Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.",
"x-example": 0
}
}
}
}
}
}
}
},
"\/account\/logs": {
@@ -535,7 +551,7 @@
"x-appwrite": {
"method": "updateMFA",
"group": "mfa",
"weight": 306,
"weight": 307,
"cookies": false,
"type": "",
"demo": "account\/update-mfa.md",
@@ -607,7 +623,7 @@
"x-appwrite": {
"method": "createMfaAuthenticator",
"group": "mfa",
"weight": 308,
"weight": 309,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-authenticator.md",
@@ -731,7 +747,7 @@
"x-appwrite": {
"method": "updateMfaAuthenticator",
"group": "mfa",
"weight": 309,
"weight": 310,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-authenticator.md",
@@ -871,7 +887,7 @@
"x-appwrite": {
"method": "deleteMfaAuthenticator",
"group": "mfa",
"weight": 310,
"weight": 311,
"cookies": false,
"type": "",
"demo": "account\/delete-mfa-authenticator.md",
@@ -995,7 +1011,7 @@
"x-appwrite": {
"method": "createMfaChallenge",
"group": "mfa",
"weight": 314,
"weight": 315,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-challenge.md",
@@ -1129,7 +1145,7 @@
"x-appwrite": {
"method": "updateMfaChallenge",
"group": "mfa",
"weight": 315,
"weight": 316,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-challenge.md",
@@ -1267,7 +1283,7 @@
"x-appwrite": {
"method": "listMfaFactors",
"group": "mfa",
"weight": 307,
"weight": 308,
"cookies": false,
"type": "",
"demo": "account\/list-mfa-factors.md",
@@ -1368,7 +1384,7 @@
"x-appwrite": {
"method": "getMfaRecoveryCodes",
"group": "mfa",
"weight": 313,
"weight": 314,
"cookies": false,
"type": "",
"demo": "account\/get-mfa-recovery-codes.md",
@@ -1467,7 +1483,7 @@
"x-appwrite": {
"method": "createMfaRecoveryCodes",
"group": "mfa",
"weight": 311,
"weight": 312,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-recovery-codes.md",
@@ -1566,7 +1582,7 @@
"x-appwrite": {
"method": "updateMfaRecoveryCodes",
"group": "mfa",
"weight": 312,
"weight": 313,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-recovery-codes.md",
@@ -2558,7 +2574,8 @@
"yandex",
"zoho",
"zoom",
"mock"
"mock",
"mock-unverified"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": []
@@ -3488,7 +3505,8 @@
"yandex",
"zoho",
"zoom",
"mock"
"mock",
"mock-unverified"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": []
@@ -5858,7 +5876,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 380,
"weight": 381,
"cookies": false,
"type": "",
"demo": "databases\/list-transactions.md",
@@ -5925,7 +5943,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 376,
"weight": 377,
"cookies": false,
"type": "",
"demo": "databases\/create-transaction.md",
@@ -5995,7 +6013,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 377,
"weight": 378,
"cookies": false,
"type": "",
"demo": "databases\/get-transaction.md",
@@ -6059,7 +6077,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 378,
"weight": 379,
"cookies": false,
"type": "",
"demo": "databases\/update-transaction.md",
@@ -6137,7 +6155,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 379,
"weight": 380,
"cookies": false,
"type": "",
"demo": "databases\/delete-transaction.md",
@@ -6203,7 +6221,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 381,
"weight": 382,
"cookies": false,
"type": "",
"demo": "databases\/create-operations.md",
@@ -6288,7 +6306,7 @@
"x-appwrite": {
"method": "listDocuments",
"group": "documents",
"weight": 339,
"weight": 340,
"cookies": false,
"type": "",
"demo": "databases\/list-documents.md",
@@ -6400,7 +6418,7 @@
"x-appwrite": {
"method": "createDocument",
"group": "documents",
"weight": 331,
"weight": 332,
"cookies": false,
"type": "",
"demo": "databases\/create-document.md",
@@ -6561,7 +6579,7 @@
"x-appwrite": {
"method": "getDocument",
"group": "documents",
"weight": 332,
"weight": 333,
"cookies": false,
"type": "",
"demo": "databases\/get-document.md",
@@ -6672,7 +6690,7 @@
"x-appwrite": {
"method": "upsertDocument",
"group": "documents",
"weight": 335,
"weight": 336,
"cookies": false,
"type": "",
"demo": "databases\/upsert-document.md",
@@ -6827,7 +6845,7 @@
"x-appwrite": {
"method": "updateDocument",
"group": "documents",
"weight": 333,
"weight": 334,
"cookies": false,
"type": "",
"demo": "databases\/update-document.md",
@@ -6939,7 +6957,7 @@
"x-appwrite": {
"method": "deleteDocument",
"group": "documents",
"weight": 337,
"weight": 338,
"cookies": false,
"type": "",
"demo": "databases\/delete-document.md",
@@ -7046,7 +7064,7 @@
"x-appwrite": {
"method": "decrementDocumentAttribute",
"group": "documents",
"weight": 342,
"weight": 343,
"cookies": false,
"type": "",
"demo": "databases\/decrement-document-attribute.md",
@@ -7173,7 +7191,7 @@
"x-appwrite": {
"method": "incrementDocumentAttribute",
"group": "documents",
"weight": 341,
"weight": 342,
"cookies": false,
"type": "",
"demo": "databases\/increment-document-attribute.md",
@@ -7300,7 +7318,7 @@
"x-appwrite": {
"method": "listExecutions",
"group": "executions",
"weight": 472,
"weight": 473,
"cookies": false,
"type": "",
"demo": "functions\/list-executions.md",
@@ -7387,7 +7405,7 @@
"x-appwrite": {
"method": "createExecution",
"group": "executions",
"weight": 470,
"weight": 471,
"cookies": false,
"type": "",
"demo": "functions\/create-execution.md",
@@ -7505,7 +7523,7 @@
"x-appwrite": {
"method": "getExecution",
"group": "executions",
"weight": 471,
"weight": 472,
"cookies": false,
"type": "",
"demo": "functions\/get-execution.md",
@@ -7580,7 +7598,7 @@
"x-appwrite": {
"method": "query",
"group": "graphql",
"weight": 241,
"weight": 242,
"cookies": false,
"type": "graphql",
"demo": "graphql\/query.md",
@@ -7634,7 +7652,7 @@
"x-appwrite": {
"method": "mutation",
"group": "graphql",
"weight": 240,
"weight": 241,
"cookies": false,
"type": "graphql",
"demo": "graphql\/mutation.md",
@@ -8120,7 +8138,7 @@
"x-appwrite": {
"method": "createSubscriber",
"group": "subscribers",
"weight": 290,
"weight": 291,
"cookies": false,
"type": "",
"demo": "messaging\/create-subscriber.md",
@@ -8204,7 +8222,7 @@
"x-appwrite": {
"method": "deleteSubscriber",
"group": "subscribers",
"weight": 294,
"weight": 295,
"cookies": false,
"type": "",
"demo": "messaging\/delete-subscriber.md",
@@ -9113,7 +9131,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 445,
"weight": 446,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-transactions.md",
@@ -9183,7 +9201,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 441,
"weight": 442,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-transaction.md",
@@ -9256,7 +9274,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 442,
"weight": 443,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-transaction.md",
@@ -9323,7 +9341,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 443,
"weight": 444,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-transaction.md",
@@ -9404,7 +9422,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 444,
"weight": 445,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-transaction.md",
@@ -9473,7 +9491,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 446,
"weight": 447,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-operations.md",
@@ -9561,7 +9579,7 @@
"x-appwrite": {
"method": "listRows",
"group": "rows",
"weight": 437,
"weight": 438,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-rows.md",
@@ -9672,7 +9690,7 @@
"x-appwrite": {
"method": "createRow",
"group": "rows",
"weight": 429,
"weight": 430,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-row.md",
@@ -9828,7 +9846,7 @@
"x-appwrite": {
"method": "getRow",
"group": "rows",
"weight": 430,
"weight": 431,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-row.md",
@@ -9938,7 +9956,7 @@
"x-appwrite": {
"method": "upsertRow",
"group": "rows",
"weight": 433,
"weight": 434,
"cookies": false,
"type": "",
"demo": "tablesdb\/upsert-row.md",
@@ -10088,7 +10106,7 @@
"x-appwrite": {
"method": "updateRow",
"group": "rows",
"weight": 431,
"weight": 432,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-row.md",
@@ -10199,7 +10217,7 @@
"x-appwrite": {
"method": "deleteRow",
"group": "rows",
"weight": 435,
"weight": 436,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-row.md",
@@ -10305,7 +10323,7 @@
"x-appwrite": {
"method": "decrementRowColumn",
"group": "rows",
"weight": 440,
"weight": 441,
"cookies": false,
"type": "",
"demo": "tablesdb\/decrement-row-column.md",
@@ -10431,7 +10449,7 @@
"x-appwrite": {
"method": "incrementRowColumn",
"group": "rows",
"weight": 439,
"weight": 440,
"cookies": false,
"type": "",
"demo": "tablesdb\/increment-row-column.md",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7393,7 +7393,7 @@
},
"attributes": {
"type": "array",
"description": "Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime, relationship), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.",
"description": "Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.",
"default": [],
"x-example": null,
"items": {
+1 -1
View File
@@ -6857,7 +6857,7 @@
},
"attributes": {
"type": "array",
"description": "Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime, relationship), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.",
"description": "Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.",
"default": [],
"x-example": null,
"items": {
+66 -47
View File
@@ -486,6 +486,23 @@
"Session": [],
"JWT": []
}
],
"parameters": [
{
"name": "payload",
"in": "body",
"schema": {
"type": "object",
"properties": {
"duration": {
"type": "integer",
"description": "Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.",
"default": 900,
"x-example": 0
}
}
}
}
]
}
},
@@ -591,7 +608,7 @@
"x-appwrite": {
"method": "updateMFA",
"group": "mfa",
"weight": 306,
"weight": 307,
"cookies": false,
"type": "",
"demo": "account\/update-mfa.md",
@@ -666,7 +683,7 @@
"x-appwrite": {
"method": "createMfaAuthenticator",
"group": "mfa",
"weight": 308,
"weight": 309,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-authenticator.md",
@@ -790,7 +807,7 @@
"x-appwrite": {
"method": "updateMfaAuthenticator",
"group": "mfa",
"weight": 309,
"weight": 310,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-authenticator.md",
@@ -931,7 +948,7 @@
"x-appwrite": {
"method": "deleteMfaAuthenticator",
"group": "mfa",
"weight": 310,
"weight": 311,
"cookies": false,
"type": "",
"demo": "account\/delete-mfa-authenticator.md",
@@ -1055,7 +1072,7 @@
"x-appwrite": {
"method": "createMfaChallenge",
"group": "mfa",
"weight": 314,
"weight": 315,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-challenge.md",
@@ -1192,7 +1209,7 @@
"x-appwrite": {
"method": "updateMfaChallenge",
"group": "mfa",
"weight": 315,
"weight": 316,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-challenge.md",
@@ -1332,7 +1349,7 @@
"x-appwrite": {
"method": "listMfaFactors",
"group": "mfa",
"weight": 307,
"weight": 308,
"cookies": false,
"type": "",
"demo": "account\/list-mfa-factors.md",
@@ -1433,7 +1450,7 @@
"x-appwrite": {
"method": "getMfaRecoveryCodes",
"group": "mfa",
"weight": 313,
"weight": 314,
"cookies": false,
"type": "",
"demo": "account\/get-mfa-recovery-codes.md",
@@ -1534,7 +1551,7 @@
"x-appwrite": {
"method": "createMfaRecoveryCodes",
"group": "mfa",
"weight": 311,
"weight": 312,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-recovery-codes.md",
@@ -1635,7 +1652,7 @@
"x-appwrite": {
"method": "updateMfaRecoveryCodes",
"group": "mfa",
"weight": 312,
"weight": 313,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-recovery-codes.md",
@@ -2667,7 +2684,8 @@
"yandex",
"zoho",
"zoom",
"mock"
"mock",
"mock-unverified"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": [],
@@ -3621,7 +3639,8 @@
"yandex",
"zoho",
"zoom",
"mock"
"mock",
"mock-unverified"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": [],
@@ -5963,7 +5982,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 380,
"weight": 381,
"cookies": false,
"type": "",
"demo": "databases\/list-transactions.md",
@@ -6030,7 +6049,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 376,
"weight": 377,
"cookies": false,
"type": "",
"demo": "databases\/create-transaction.md",
@@ -6100,7 +6119,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 377,
"weight": 378,
"cookies": false,
"type": "",
"demo": "databases\/get-transaction.md",
@@ -6163,7 +6182,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 378,
"weight": 379,
"cookies": false,
"type": "",
"demo": "databases\/update-transaction.md",
@@ -6242,7 +6261,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 379,
"weight": 380,
"cookies": false,
"type": "",
"demo": "databases\/delete-transaction.md",
@@ -6307,7 +6326,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 381,
"weight": 382,
"cookies": false,
"type": "",
"demo": "databases\/create-operations.md",
@@ -6388,7 +6407,7 @@
"x-appwrite": {
"method": "listDocuments",
"group": "documents",
"weight": 339,
"weight": 340,
"cookies": false,
"type": "",
"demo": "databases\/list-documents.md",
@@ -6492,7 +6511,7 @@
"x-appwrite": {
"method": "createDocument",
"group": "documents",
"weight": 331,
"weight": 332,
"cookies": false,
"type": "",
"demo": "databases\/create-document.md",
@@ -6651,7 +6670,7 @@
"x-appwrite": {
"method": "getDocument",
"group": "documents",
"weight": 332,
"weight": 333,
"cookies": false,
"type": "",
"demo": "databases\/get-document.md",
@@ -6754,7 +6773,7 @@
"x-appwrite": {
"method": "upsertDocument",
"group": "documents",
"weight": 335,
"weight": 336,
"cookies": false,
"type": "",
"demo": "databases\/upsert-document.md",
@@ -6905,7 +6924,7 @@
"x-appwrite": {
"method": "updateDocument",
"group": "documents",
"weight": 333,
"weight": 334,
"cookies": false,
"type": "",
"demo": "databases\/update-document.md",
@@ -7015,7 +7034,7 @@
"x-appwrite": {
"method": "deleteDocument",
"group": "documents",
"weight": 337,
"weight": 338,
"cookies": false,
"type": "",
"demo": "databases\/delete-document.md",
@@ -7116,7 +7135,7 @@
"x-appwrite": {
"method": "decrementDocumentAttribute",
"group": "documents",
"weight": 342,
"weight": 343,
"cookies": false,
"type": "",
"demo": "databases\/decrement-document-attribute.md",
@@ -7237,7 +7256,7 @@
"x-appwrite": {
"method": "incrementDocumentAttribute",
"group": "documents",
"weight": 341,
"weight": 342,
"cookies": false,
"type": "",
"demo": "databases\/increment-document-attribute.md",
@@ -7356,7 +7375,7 @@
"x-appwrite": {
"method": "listExecutions",
"group": "executions",
"weight": 472,
"weight": 473,
"cookies": false,
"type": "",
"demo": "functions\/list-executions.md",
@@ -7439,7 +7458,7 @@
"x-appwrite": {
"method": "createExecution",
"group": "executions",
"weight": 470,
"weight": 471,
"cookies": false,
"type": "",
"demo": "functions\/create-execution.md",
@@ -7558,7 +7577,7 @@
"x-appwrite": {
"method": "getExecution",
"group": "executions",
"weight": 471,
"weight": 472,
"cookies": false,
"type": "",
"demo": "functions\/get-execution.md",
@@ -7630,7 +7649,7 @@
"x-appwrite": {
"method": "query",
"group": "graphql",
"weight": 241,
"weight": 242,
"cookies": false,
"type": "graphql",
"demo": "graphql\/query.md",
@@ -7705,7 +7724,7 @@
"x-appwrite": {
"method": "mutation",
"group": "graphql",
"weight": 240,
"weight": 241,
"cookies": false,
"type": "graphql",
"demo": "graphql\/mutation.md",
@@ -8204,7 +8223,7 @@
"x-appwrite": {
"method": "createSubscriber",
"group": "subscribers",
"weight": 290,
"weight": 291,
"cookies": false,
"type": "",
"demo": "messaging\/create-subscriber.md",
@@ -8289,7 +8308,7 @@
"x-appwrite": {
"method": "deleteSubscriber",
"group": "subscribers",
"weight": 294,
"weight": 295,
"cookies": false,
"type": "",
"demo": "messaging\/delete-subscriber.md",
@@ -9146,7 +9165,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 445,
"weight": 446,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-transactions.md",
@@ -9216,7 +9235,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 441,
"weight": 442,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-transaction.md",
@@ -9289,7 +9308,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 442,
"weight": 443,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-transaction.md",
@@ -9355,7 +9374,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 443,
"weight": 444,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-transaction.md",
@@ -9437,7 +9456,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 444,
"weight": 445,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-transaction.md",
@@ -9505,7 +9524,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 446,
"weight": 447,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-operations.md",
@@ -9589,7 +9608,7 @@
"x-appwrite": {
"method": "listRows",
"group": "rows",
"weight": 437,
"weight": 438,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-rows.md",
@@ -9692,7 +9711,7 @@
"x-appwrite": {
"method": "createRow",
"group": "rows",
"weight": 429,
"weight": 430,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-row.md",
@@ -9846,7 +9865,7 @@
"x-appwrite": {
"method": "getRow",
"group": "rows",
"weight": 430,
"weight": 431,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-row.md",
@@ -9948,7 +9967,7 @@
"x-appwrite": {
"method": "upsertRow",
"group": "rows",
"weight": 433,
"weight": 434,
"cookies": false,
"type": "",
"demo": "tablesdb\/upsert-row.md",
@@ -10094,7 +10113,7 @@
"x-appwrite": {
"method": "updateRow",
"group": "rows",
"weight": 431,
"weight": 432,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-row.md",
@@ -10203,7 +10222,7 @@
"x-appwrite": {
"method": "deleteRow",
"group": "rows",
"weight": 435,
"weight": 436,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-row.md",
@@ -10303,7 +10322,7 @@
"x-appwrite": {
"method": "decrementRowColumn",
"group": "rows",
"weight": 440,
"weight": 441,
"cookies": false,
"type": "",
"demo": "tablesdb\/decrement-row-column.md",
@@ -10423,7 +10442,7 @@
"x-appwrite": {
"method": "incrementRowColumn",
"group": "rows",
"weight": 439,
"weight": 440,
"cookies": false,
"type": "",
"demo": "tablesdb\/increment-row-column.md",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+21 -15
View File
@@ -68,6 +68,7 @@ use Utopia\Validator;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -1634,9 +1635,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::USER_UNAUTHORIZED, 'OAuth provider failed to return email.');
}
/**
* Is verified is not used yet, since we don't know after an account is created anymore if it was verified or not.
*/
$isVerified = $oauth2->isEmailVerified($accessToken);
$identity = $dbForProject->findOne('identities', [
@@ -1648,16 +1646,32 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$user = $dbForProject->getDocument('users', $identity->getAttribute('userId'));
}
// If user is not found, check if there is an identity with the same provider user ID
// If user is not found, check if there is a user with the same email
if ($user === false || $user->isEmpty()) {
$userWithEmail = $dbForProject->findOne('users', [
Query::equal('email', [$email]),
]);
if (!$userWithEmail->isEmpty()) {
if (!$isVerified) {
$failureRedirect(Exception::GENERAL_BAD_REQUEST);
}
$user->setAttributes($userWithEmail->getArrayCopy());
}
}
// If user is not found, check if there is an identity with the same email
if ($user === false || $user->isEmpty()) {
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if (!$identityWithMatchingEmail->isEmpty()) {
if (!$isVerified) {
$failureRedirect(Exception::GENERAL_BAD_REQUEST);
}
$user->setAttributes($dbForProject->getDocument('users', $identityWithMatchingEmail->getAttribute('userId'))->getArrayCopy());
}
}
if ($user === false || $user->isEmpty()) { // Last option -> create the user
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
@@ -1669,14 +1683,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
}
}
// Makes sure this email is not already used in another identity
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if (!$identityWithMatchingEmail->isEmpty()) {
$failureRedirect(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
}
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
@@ -1730,7 +1736,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'providerType' => MESSAGE_TYPE_EMAIL,
'identifier' => $email,
]));
} catch (Duplicate) {
$failureRedirect(Exception::USER_ALREADY_EXISTS);
}
@@ -2949,20 +2954,21 @@ App::post('/v1/account/jwts')
],
contentType: ContentType::JSON,
))
->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true)
->label('abuse-limit', 100)
->label('abuse-key', 'url:{url},userId:{userId}')
->inject('response')
->inject('user')
->inject('store')
->inject('proofForToken')
->action(function (Response $response, User $user, Store $store, ProofsToken $proofForToken) {
->action(function (int $duration, Response $response, User $user, Store $store, ProofsToken $proofForToken) {
$sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken);
if (!$sessionId) {
throw new Exception(Exception::USER_SESSION_NOT_FOUND);
}
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0);
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
+4 -3
View File
@@ -294,7 +294,7 @@ App::post('/v1/projects')
// Hook allowing instant project mirroring during migration
// Outside of migration, hook is not registered and has no effect
$hooks->trigger('afterProjectCreation', [ $project, $pools, $cache ]);
$hooks->trigger('afterProjectCreation', [$project, $pools, $cache]);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -2079,6 +2079,7 @@ App::patch('/v1/projects/:projectId/smtp')
if ($enabled) {
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->SMTPAuth = (!empty($username) && !empty($password));
$mail->Username = $username;
$mail->Password = $password;
$mail->Host = $host;
@@ -2094,7 +2095,7 @@ App::patch('/v1/projects/:projectId/smtp')
throw new Exception('Connection is not valid.');
}
} catch (Throwable $error) {
throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, 'Could not connect to SMTP server: ' . $error->getMessage());
throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, $error->getMessage());
}
}
@@ -2665,7 +2666,7 @@ App::patch('/v1/projects/:projectId/auth/session-invalidation')
$auths = $project->getAttribute('auths', []);
$auths['invalidateSessions'] = $enabled;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
+22
View File
@@ -117,6 +117,28 @@ App::get('/v1/mock/tests/general/oauth2/user')
'id' => 1,
'name' => 'User Name',
'email' => 'useroauth@localhost.test',
'verified' => true,
]);
});
App::get('/v1/mock/tests/general/oauth2/user-unverified')
->desc('OAuth2 User Unverified')
->groups(['mock'])
->label('scope', 'public')
->label('docs', false)
->param('token', '', new Text(100), 'OAuth2 Access Token.')
->inject('response')
->action(function (string $token, Response $response) {
if ($token != '123456') {
throw new Exception(Exception::GENERAL_MOCK, 'Invalid token');
}
$response->json([
'id' => 2,
'name' => 'User Name Unverified',
'email' => 'useroauthunverified@localhost.test',
'verified' => false,
]);
});
+1
View File
@@ -275,6 +275,7 @@ App::setResource('cors', fn (array $allowedHostnames) => new Cors(
'X-Appwrite-ID',
'X-Appwrite-Timestamp',
'X-Appwrite-Session',
'X-Appwrite-Platform', // for `$platform` injection and SDK generator
// SDK generator
'X-SDK-Version',
'X-SDK-Name',
+3
View File
@@ -1171,6 +1171,9 @@ services:
- "traefik.http.routers.appwrite_maildev_https.rule=Host(`mail.localhost`)"
- "traefik.http.routers.appwrite_maildev_https.service=appwrite_maildev"
- "traefik.http.routers.appwrite_maildev_https.tls=true"
environment:
- MAILDEV_INCOMING_USER=${_APP_SMTP_USERNAME}
- MAILDEV_INCOMING_PASS=${_APP_SMTP_PASSWORD}
request-catcher-webhook: # used mainly for dev tests (mock HTTP webhook)
image: appwrite/requestcatcher:1.0.0
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## 20.1.0
* Added ability to create columns and indexes synchronously while creating a table
## 20.0.0
* Rename `VCSDeploymentType` enum to `VCSReferenceType`
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## 0.24.0
* Added ability to create columns and indexes synchronously while creating a table
## 0.23.0
* Rename `VCSDeploymentType` enum to `VCSReferenceType`
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## v0.16.0
* Added ability to create columns and indexes synchronously while creating a table
## v0.15.0
* Rename `VCSDeploymentType` enum to `VCSReferenceType`
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## 13.1.0
* Added ability to create columns and indexes synchronously while creating a table
## 13.0.0
* Rename `VCSDeploymentType` enum to `VCSReferenceType`
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## 21.1.0
* Added ability to create columns and indexes synchronously while creating a table
## 21.0.0
* Rename `VCSDeploymentType` enum to `VCSReferenceType`
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## 19.1.0
* Added ability to create columns and indexes synchronously while creating a table
## 19.0.0
* Rename `VCSDeploymentType` enum to `VCSReferenceType`
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## 14.1.0
* Added ability to create columns and indexes synchronously while creating a table
## 14.0.0
* Rename `VCSDeploymentType` enum to `VCSReferenceType`
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## 20.1.0
* Added ability to create columns and indexes synchronously while creating a table
## 20.0.0
* Rename `VCSDeploymentType` enum to `VCSReferenceType`
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## 14.1.0
* Added ability to create columns and indexes synchronously while creating a table
## 14.0.0
* Rename `VCSDeploymentType` enum to `VCSReferenceType`
+3 -1
View File
@@ -130,7 +130,9 @@ class Mock extends OAuth2
*/
public function isEmailVerified(string $accessToken): bool
{
return true;
$user = $this->getUser($accessToken);
return $user['verified'] ?? true;
}
/**
@@ -0,0 +1,30 @@
<?php
namespace Appwrite\Auth\OAuth2;
class MockUnverified extends Mock
{
/**
* @return string
*/
public function getName(): string
{
return 'mock-unverified';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken): array
{
if (empty($this->user)) {
$user = $this->request('GET', 'http://localhost/' . $this->version . '/mock/tests/general/oauth2/user-unverified?token=' . \urlencode($accessToken));
$this->user = \json_decode($user, true);
}
return $this->user;
}
}
+9 -2
View File
@@ -107,9 +107,11 @@ class Action extends UtopiaAction
}
}
public function disableSubqueries()
public function disableSubqueries(array $filters = []): void
{
$filters = $this->filters;
if (empty($filters)) {
$filters = $this->filters;
}
foreach ($filters as $filter) {
Database::addFilter(
@@ -189,6 +191,11 @@ class Action extends UtopiaAction
}
}
// found a wildcard, return!
if (\in_array('*', $attributes)) {
return;
}
$responseModel = $response->getModel($model);
foreach ($responseModel->getRules() as $ruleName => $rule) {
if (\str_starts_with($ruleName, '$')) {
@@ -80,7 +80,7 @@ class Create extends Action
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
->param('attributes', [], new ArrayList(new JSON(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime, relationship), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.', true)
->param('attributes', [], new ArrayList(new JSON(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.', true)
->param('indexes', [], new ArrayList(new JSON(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index definitions to create. Each index should contain: key (string), type (string: key, fulltext, unique, spatial), attributes (array of attribute keys), orders (array of ASC/DESC, optional), and lengths (array of integers, optional).', true)
->inject('response')
->inject('dbForProject')
@@ -34,7 +34,7 @@ class Create extends Action
->setHttpPath('/v1/projects/:projectId/dev-keys')
->desc('Create dev key')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('scope', 'devKeys.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'devKeys',
@@ -28,7 +28,7 @@ class Delete extends Action
->setHttpPath('/v1/projects/:projectId/dev-keys/:keyId')
->desc('Delete dev key')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('scope', 'devKeys.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'devKeys',
@@ -28,7 +28,7 @@ class Get extends Action
->setHttpPath('/v1/projects/:projectId/dev-keys/:keyId')
->desc('Get dev key')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('scope', 'devKeys.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'devKeys',
@@ -29,7 +29,7 @@ class Update extends Action
->setHttpPath('/v1/projects/:projectId/dev-keys/:keyId')
->desc('Update dev key')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('scope', 'devKeys.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'devKeys',
@@ -32,7 +32,7 @@ class XList extends Action
->setHttpPath('/v1/projects/:projectId/dev-keys')
->desc('List dev keys')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('scope', 'devKeys.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'devKeys',
@@ -3,19 +3,21 @@
namespace Appwrite\Platform\Modules\Projects\Http\Projects;
use Appwrite\Extend\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\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator;
use Utopia\Validator\Boolean;
@@ -24,6 +26,10 @@ use Utopia\Validator\Text;
class XList extends Action
{
use HTTP;
// cached mapping of columns to their subQuery filters
private static ?array $attributeToSubQueryFilters = null;
public static function getName()
{
return 'listProjects';
@@ -61,12 +67,13 @@ class XList extends Action
->param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('request')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform)
public function action(array $queries, string $search, bool $includeTotal, Request $request, Response $response, Database $dbForPlatform)
{
try {
$queries = Query::parseQueries($queries);
@@ -103,16 +110,89 @@ class XList extends Action
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$projects = $dbForPlatform->find('projects', $queries);
$selectQueries = Query::groupByType($queries)['selections'] ?? [];
$filterQueries = Query::groupByType($queries)['filters'];
$projects = $this->find($dbForPlatform, $queries, $selectQueries);
$total = $includeTotal ? $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (Order $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$this->applySelectQueries($request, $response, Response::MODEL_PROJECT);
$response->dynamic(new Document([
'projects' => $projects,
'total' => $total,
]), Response::MODEL_PROJECT_LIST);
}
// Build mapping of columns to their subQuery filters
private static function getAttributeToSubQueryFilters(): array
{
if (self::$attributeToSubQueryFilters !== null) {
return self::$attributeToSubQueryFilters;
}
self::$attributeToSubQueryFilters = [];
$collections = Config::getParam('collections', []);
$projectAttributes = $collections['platform']['projects']['attributes'] ?? [];
foreach ($projectAttributes as $attribute) {
$attributeId = $attribute['$id'] ?? null;
$filters = $attribute['filters'] ?? [];
if ($attributeId === null || empty($filters)) {
continue;
}
// extract only subQuery filters
$subQueryFilters = \array_filter($filters, function ($filter) {
return \str_starts_with($filter, 'subQuery');
});
if (!empty($subQueryFilters)) {
self::$attributeToSubQueryFilters[$attributeId] = \array_values($subQueryFilters);
}
}
return self::$attributeToSubQueryFilters;
}
private function find(Database $dbForPlatform, array $queries, array $selectQueries): array
{
if (empty($selectQueries)) {
return $dbForPlatform->find('projects', $queries);
}
$selectedAttributes = [];
foreach ($selectQueries as $query) {
foreach ($query->getValues() as $value) {
$selectedAttributes[] = $value;
}
}
if (\in_array('*', $selectedAttributes)) {
return $dbForPlatform->find('projects', $queries);
}
$filtersToSkipMap = [];
$selectedAttributesMap = \array_flip($selectedAttributes);
$attributeToSubQueryFilters = self::getAttributeToSubQueryFilters();
foreach ($attributeToSubQueryFilters as $attributeName => $subQueryFilters) {
if (!isset($selectedAttributesMap[$attributeName])) {
foreach ($subQueryFilters as $filter) {
$filtersToSkipMap[$filter] = true;
}
}
}
$filtersToSkip = \array_keys($filtersToSkipMap);
return empty($filtersToSkip)
? $dbForPlatform->find('projects', $queries)
: $dbForPlatform->skipFilters(fn () => $dbForPlatform->find('projects', $queries), $filtersToSkip);
}
}
+1 -1
View File
@@ -55,7 +55,7 @@ class SDKs extends Action
public function action(?string $selectedPlatform, ?string $selectedSDK, ?string $version, ?string $git, ?string $production, ?string $message, ?string $release, ?string $commit, ?string $sdks): void
{
if (!$sdks) {
$selectedPlatform ??= Console::confirm('Choose Platform ("' . APP_SDK_PLATFORM_CLIENT . '", "' . APP_SDK_PLATFORM_SERVER . '", "' . APP_SDK_PLATFORM_CONSOLE . '" or "*" for all):');
$selectedPlatform ??= Console::confirm('Choose Platform ("' . implode('", "', Specs::getPlatforms()) . '" or "*" for all):');
$selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):'));
} else {
$sdks = explode(',', $sdks);
+2 -2
View File
@@ -66,7 +66,7 @@ class Specs extends Action
*
* @return array<string>
*/
protected function getPlatforms(): array
public static function getPlatforms(): array
{
return [
APP_SDK_PLATFORM_CLIENT,
@@ -239,7 +239,7 @@ class Specs extends Action
App::setResource('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None())));
App::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
$platforms = $this->getPlatforms();
$platforms = self::getPlatforms();
$authCounts = $this->getAuthCounts();
$keys = $this->getKeys();
@@ -17,4 +17,9 @@ class Projects extends Base
{
parent::__construct('projects', self::ALLOWED_ATTRIBUTES);
}
public function isSelectQueryAllowed(): bool
{
return true;
}
}
+36 -6
View File
@@ -341,6 +341,20 @@ class Project extends Model
*/
public function filter(Document $document): Document
{
$this->expandSmtpFields($document);
$this->expandServiceFields($document);
$this->expandAuthFields($document);
$this->expandOAuthProviders($document);
return $document;
}
private function expandSmtpFields(Document $document): void
{
if (!$document->isSet('smtp')) {
return;
}
// SMTP
$smtp = $document->getAttribute('smtp', []);
$document->setAttribute('smtpEnabled', $smtp['enabled'] ?? false);
@@ -352,8 +366,14 @@ class Project extends Model
$document->setAttribute('smtpUsername', $smtp['username'] ?? '');
$document->setAttribute('smtpPassword', $smtp['password'] ?? '');
$document->setAttribute('smtpSecure', $smtp['secure'] ?? '');
}
private function expandServiceFields(Document $document): void
{
if (!$document->isSet('services')) {
return;
}
// Services
$values = $document->getAttribute('services', []);
$services = Config::getParam('services', []);
@@ -365,8 +385,14 @@ class Project extends Model
$value = $values[$key] ?? true;
$document->setAttribute('serviceStatusFor' . ucfirst($key), $value);
}
}
private function expandAuthFields(Document $document): void
{
if (!$document->isSet('auths')) {
return;
}
// Auth
$authValues = $document->getAttribute('auths', []);
$auth = Config::getParam('auth', []);
@@ -383,13 +409,19 @@ class Project extends Model
$document->setAttribute('authMembershipsMfa', $authValues['membershipsMfa'] ?? true);
$document->setAttribute('authInvalidateSessions', $authValues['invalidateSessions'] ?? false);
foreach ($auth as $index => $method) {
foreach ($auth as $method) {
$key = $method['key'];
$value = $authValues[$key] ?? true;
$document->setAttribute('auth' . ucfirst($key), $value);
}
}
private function expandOAuthProviders(Document $document): void
{
if (!$document->isSet('oAuthProviders')) {
return;
}
// OAuth Providers
$providers = Config::getParam('oAuthProviders', []);
$providerValues = $document->getAttribute('oAuthProviders', []);
$projectProviders = [];
@@ -410,7 +442,5 @@ class Project extends Model
}
$document->setAttribute('oAuthProviders', $projectProviders);
return $document;
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ class HTTPTest extends Scope
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEquals('Appwrite', $response['headers']['server']);
$this->assertEquals('GET, POST, PUT, PATCH, DELETE', $response['headers']['access-control-allow-methods']);
$this->assertEquals('Accept, Origin, Cookie, Set-Cookie, Content-Type, Content-Range, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-Appwrite-ID, X-Appwrite-Timestamp, X-Appwrite-Session, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-SDK-Profile, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies, X-Requested-With, X-Forwarded-For, X-Forwarded-User-Agent', $response['headers']['access-control-allow-headers']);
$this->assertEquals('Accept, Origin, Cookie, Set-Cookie, Content-Type, Content-Range, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-Appwrite-ID, X-Appwrite-Timestamp, X-Appwrite-Session, X-Appwrite-Platform, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-SDK-Profile, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies, X-Requested-With, X-Forwarded-For, X-Forwarded-User-Agent', $response['headers']['access-control-allow-headers']);
$this->assertEquals('X-Appwrite-Session, X-Fallback-Cookies', $response['headers']['access-control-expose-headers']);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
$this->assertEquals('true', $response['headers']['access-control-allow-credentials']);
+3 -3
View File
@@ -161,9 +161,9 @@ trait ProjectCustom
'senderEmail' => 'mailer@appwrite.io',
'senderName' => 'Mailer',
'host' => 'maildev',
'port' => 1025,
'username' => '',
'password' => '',
'port' => intval(System::getEnv('_APP_SMTP_PORT', "1025")),
'username' => System::getEnv('_APP_SMTP_USERNAME', 'user'),
'password' => System::getEnv('_APP_SMTP_PASSWORD', 'password'),
]);
$project = [
@@ -1887,6 +1887,57 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(401, $response['headers']['status-code']);
// Test JWT with custom duration
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$response = $this->client->call(Client::METHOD_POST, '/account/jwt', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'duration' => 5
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['jwt']);
$jwt = $response['body']['jwt'];
// Ensure JWT works before expiration
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-jwt' => $jwt,
]));
$this->assertEquals(200, $response['headers']['status-code']);
// Wait for JWT to expire
\sleep(6);
// Ensure JWT no longer works after expiration
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-jwt' => $jwt,
]));
$this->assertEquals(401, $response['headers']['status-code']);
return [];
}
@@ -2183,6 +2234,157 @@ class AccountCustomClientTest extends Scope
$this->assertEquals('tuvwxyz', $response['body']['providerRefreshToken']);
$this->assertNotEquals($initialExpiry, $response['body']['providerAccessTokenExpiry']);
// Clean up - delete the user
$response = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals(204, $response['headers']['status-code']);
return [];
}
public function testOAuthUnverifiedEmailCannotLinkToExistingAccount()
{
$provider = 'mock-unverified';
$appId = '1';
$secret = '123456';
// First, create a user with the same email that the unverified OAuth will try to use
$email = 'useroauthunverified@localhost.test';
$password = 'password';
$response = $this->client->call(Client::METHOD_POST, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$existingUserId = $response['body']['$id'];
// Enable the mock-unverified provider
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $this->getProject()['$id'] . '/oauth2', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'provider' => $provider,
'appId' => $appId,
'secret' => $secret,
'enabled' => true,
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Attempt OAuth login with unverified email - should fail because existing user has same email
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'success' => 'http://localhost/v1/mock/tests/general/oauth2/success',
'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure',
]);
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertEquals('failure', $response['body']['result']);
// Clean up - delete the user
$response = $this->client->call(Client::METHOD_DELETE, '/users/' . $existingUserId, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals(204, $response['headers']['status-code']);
return [];
}
public function testOAuthVerifiedEmailCanLinkToExistingAccount()
{
$provider = 'mock';
$appId = '1';
$secret = '123456';
$email = 'useroauth@localhost.test';
// Create a user with the same email that the verified OAuth will try to use
$response = $this->client->call(Client::METHOD_POST, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'userId' => ID::unique(),
'email' => $email,
'password' => 'password',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$existingUserId = $response['body']['$id'];
// Enable the mock provider
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $this->getProject()['$id'] . '/oauth2', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'provider' => $provider,
'appId' => $appId,
'secret' => $secret,
'enabled' => true,
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Attempt OAuth login with verified email - should succeed and link to existing account
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'success' => 'http://localhost/v1/mock/tests/general/oauth2/success',
'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('success', $response['body']['result']);
// Verify the OAuth identity was linked to the existing user
$sessionCookieKey = 'a_session_' . $this->getProject()['$id'];
$session = $response['cookies'][$sessionCookieKey];
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($existingUserId, $response['body']['$id']);
$this->assertEquals($email, $response['body']['email']);
// Clean up - delete the user
$response = $this->client->call(Client::METHOD_DELETE, '/users/' . $existingUserId, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals(204, $response['headers']['status-code']);
return [];
}
@@ -23,9 +23,9 @@ class ProjectsConsoleClientTest extends Scope
use Async;
/**
* @group devKeys
* @group smtpAndTemplates
* @group projectsCRUD */
* @group projectsCRUD
*/
public function testCreateProject(): array
{
/**
@@ -257,11 +257,11 @@ class ProjectsConsoleClientTest extends Scope
'search' => $id
]));
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals($response['body']['total'], 3);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(4, $response['body']['total']);
$this->assertIsArray($response['body']['projects']);
$this->assertCount(3, $response['body']['projects']);
$this->assertEquals($response['body']['projects'][0]['name'], 'Project Test');
$this->assertCount(4, $response['body']['projects']);
$this->assertEquals('Project Test', $response['body']['projects'][0]['name']);
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
@@ -271,9 +271,9 @@ class ProjectsConsoleClientTest extends Scope
]));
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(3, $response['body']['total']);
$this->assertEquals(4, $response['body']['total']);
$this->assertIsArray($response['body']['projects']);
$this->assertCount(3, $response['body']['projects']);
$this->assertCount(4, $response['body']['projects']);
$this->assertEquals($response['body']['projects'][0]['$id'], $data['projectId']);
/**
@@ -348,8 +348,8 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertCount(1, $response['body']['projects']);
$this->assertEquals('Project Test 2', $response['body']['projects'][0]['name']);
$this->assertCount(2, $response['body']['projects']);
$this->assertEquals('Team 1 Project', $response['body']['projects'][0]['name']);
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
@@ -376,7 +376,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertCount(4, $response['body']['projects']);
$this->assertCount(5, $response['body']['projects']);
$this->assertEquals('Project Test 2', $response['body']['projects'][0]['name']);
$this->assertEquals('Team 1 Project', $response['body']['projects'][1]['name']);
@@ -387,9 +387,9 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertCount(4, $response['body']['projects']);
$this->assertCount(5, $response['body']['projects']);
$this->assertEquals('Project Test', $response['body']['projects'][0]['name']);
$this->assertEquals('Team 1 Project', $response['body']['projects'][2]['name']);
$this->assertEquals('Original Project', $response['body']['projects'][2]['name']);
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
@@ -402,8 +402,8 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertCount(3, $response['body']['projects']);
$this->assertEquals('Team 1 Project', $response['body']['projects'][1]['name']);
$this->assertCount(4, $response['body']['projects']);
$this->assertEquals('Original Project', $response['body']['projects'][1]['name']);
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
@@ -436,6 +436,267 @@ class ProjectsConsoleClientTest extends Scope
return $data;
}
/**
* @group projectsCRUD
*/
public function testListProjectsQuerySelect(): void
{
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => ID::unique(),
'name' => 'Query Select Test Team',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$teamId = $team['body']['$id'];
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Query Select Test Project',
'teamId' => $teamId,
'region' => System::getEnv('_APP_REGION', 'default')
]);
$this->assertEquals(201, $project['headers']['status-code']);
$projectId = $project['body']['$id'];
/**
* Test Query.select - basic fields
*/
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name'])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertGreaterThan(0, count($response['body']['projects']));
$project = $response['body']['projects'][0];
$this->assertArrayHasKey('$id', $project);
$this->assertArrayHasKey('name', $project);
$this->assertArrayNotHasKey('platforms', $project);
$this->assertArrayNotHasKey('webhooks', $project);
$this->assertArrayNotHasKey('keys', $project);
$this->assertArrayNotHasKey('devKeys', $project);
$this->assertArrayNotHasKey('oAuthProviders', $project);
$this->assertArrayNotHasKey('smtpEnabled', $project);
$this->assertArrayNotHasKey('smtpHost', $project);
$this->assertArrayNotHasKey('authLimit', $project);
$this->assertArrayNotHasKey('authDuration', $project);
/**
* Test Query.select - multiple fields
*/
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'teamId', 'description', '$createdAt', '$updatedAt'])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertGreaterThan(0, count($response['body']['projects']));
$project = $response['body']['projects'][0];
$this->assertArrayHasKey('$id', $project);
$this->assertArrayHasKey('name', $project);
$this->assertArrayHasKey('teamId', $project);
$this->assertArrayHasKey('description', $project);
$this->assertArrayHasKey('$createdAt', $project);
$this->assertArrayHasKey('$updatedAt', $project);
$this->assertArrayNotHasKey('platforms', $project);
$this->assertArrayNotHasKey('webhooks', $project);
$this->assertArrayNotHasKey('keys', $project);
$this->assertArrayNotHasKey('devKeys', $project);
$this->assertArrayNotHasKey('oAuthProviders', $project);
$this->assertArrayNotHasKey('smtpEnabled', $project);
$this->assertArrayNotHasKey('authLimit', $project);
/**
* Test Query.select combined with filters
*/
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'teamId'])->toString(),
Query::equal('name', ['Query Select Test Project'])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertCount(1, $response['body']['projects']);
$project = $response['body']['projects'][0];
$this->assertArrayHasKey('$id', $project);
$this->assertArrayHasKey('name', $project);
$this->assertArrayHasKey('teamId', $project);
$this->assertEquals('Query Select Test Project', $project['name']);
$this->assertEquals($teamId, $project['teamId']);
$this->assertArrayNotHasKey('platforms', $project);
$this->assertArrayNotHasKey('webhooks', $project);
$this->assertArrayNotHasKey('keys', $project);
$this->assertArrayNotHasKey('devKeys', $project);
$this->assertArrayNotHasKey('oAuthProviders', $project);
$this->assertArrayNotHasKey('smtpEnabled', $project);
$this->assertArrayNotHasKey('authLimit', $project);
/**
* Test Query.select combined with limit
*/
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name'])->toString(),
Query::limit(2)->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertLessThanOrEqual(2, count($response['body']['projects']));
foreach ($response['body']['projects'] as $p) {
$this->assertArrayHasKey('$id', $p);
$this->assertArrayHasKey('name', $p);
$this->assertArrayNotHasKey('platforms', $p);
$this->assertArrayNotHasKey('webhooks', $p);
$this->assertArrayNotHasKey('keys', $p);
$this->assertArrayNotHasKey('devKeys', $p);
$this->assertArrayNotHasKey('oAuthProviders', $p);
$this->assertArrayNotHasKey('smtpEnabled', $p);
$this->assertArrayNotHasKey('authLimit', $p);
}
/**
* Test Query.select with subquery attributes (platforms, webhooks, etc.)
* When explicitly selected, subqueries should still run
*/
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'platforms'])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertGreaterThan(0, count($response['body']['projects']));
$project = $response['body']['projects'][0];
$this->assertArrayHasKey('$id', $project);
$this->assertArrayHasKey('name', $project);
$this->assertArrayHasKey('platforms', $project);
$this->assertIsArray($project['platforms']);
$this->assertArrayNotHasKey('webhooks', $project);
$this->assertArrayNotHasKey('keys', $project);
$this->assertArrayNotHasKey('devKeys', $project);
$this->assertArrayNotHasKey('oAuthProviders', $project);
$this->assertArrayNotHasKey('smtpEnabled', $project);
$this->assertArrayNotHasKey('authLimit', $project);
/**
* Test Query.select with expanded attributes
* webhooks and keys should load their subquery data when selected
*/
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'webhooks', 'keys'])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertGreaterThan(0, count($response['body']['projects']));
$project = $response['body']['projects'][0];
$this->assertArrayHasKey('$id', $project);
$this->assertArrayHasKey('name', $project);
$this->assertArrayHasKey('webhooks', $project);
$this->assertArrayHasKey('keys', $project);
$this->assertIsArray($project['webhooks']);
$this->assertIsArray($project['keys']);
$this->assertArrayNotHasKey('platforms', $project);
$this->assertArrayNotHasKey('devKeys', $project);
$this->assertArrayNotHasKey('smtpEnabled', $project);
$this->assertArrayNotHasKey('authLimit', $project);
/**
* Test Query.select with wildcard '*'
* Should return all fields like no select query
*/
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::select(['*'])->toString(),
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertGreaterThan(0, count($response['body']['projects']));
$project = $response['body']['projects'][0];
$this->assertArrayHasKey('$id', $project);
$this->assertArrayHasKey('name', $project);
$this->assertArrayHasKey('teamId', $project);
$this->assertArrayHasKey('platforms', $project);
$this->assertArrayHasKey('webhooks', $project);
$this->assertArrayHasKey('keys', $project);
$this->assertArrayHasKey('devKeys', $project);
$this->assertArrayHasKey('oAuthProviders', $project);
$this->assertArrayHasKey('smtpEnabled', $project);
$this->assertArrayHasKey('smtpHost', $project);
$this->assertArrayHasKey('authLimit', $project);
$this->assertArrayHasKey('authDuration', $project);
/**
* Test Query.select with invalid attribute
*/
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'invalidAttribute'])->toString(),
],
]);
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertEquals('Invalid `queries` param: Invalid query: Attribute not found in schema: invalidAttribute', $response['body']['message']);
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
}
public function testGetProject(): void
{
// Create a team
@@ -618,6 +879,14 @@ class ProjectsConsoleClientTest extends Scope
public function testUpdateProjectSMTP($data): array
{
$id = $data['projectId'];
$smtpHost = System::getEnv('_APP_SMTP_HOST', "maildev");
$smtpPort = intval(System::getEnv('_APP_SMTP_PORT', "1025"));
$smtpUsername = System::getEnv('_APP_SMTP_USERNAME', 'user');
$smtpPassword = System::getEnv('_APP_SMTP_PASSWORD', 'password');
/**
* Test for SUCCESS: Valid Credentials
*/
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -625,23 +894,23 @@ class ProjectsConsoleClientTest extends Scope
'enabled' => true,
'senderEmail' => 'mailer@appwrite.io',
'senderName' => 'Mailer',
'host' => 'maildev',
'port' => 1025,
'username' => 'user',
'password' => 'password',
'host' => $smtpHost,
'port' => $smtpPort,
'username' => $smtpUsername,
'password' => $smtpPassword,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertTrue($response['body']['smtpEnabled']);
$this->assertEquals('mailer@appwrite.io', $response['body']['smtpSenderEmail']);
$this->assertEquals('Mailer', $response['body']['smtpSenderName']);
$this->assertEquals('maildev', $response['body']['smtpHost']);
$this->assertEquals(1025, $response['body']['smtpPort']);
$this->assertEquals('user', $response['body']['smtpUsername']);
$this->assertEquals('password', $response['body']['smtpPassword']);
$this->assertEquals($smtpHost, $response['body']['smtpHost']);
$this->assertEquals($smtpPort, $response['body']['smtpPort']);
$this->assertEquals($smtpUsername, $response['body']['smtpUsername']);
$this->assertEquals($smtpPassword, $response['body']['smtpPassword']);
$this->assertEquals('', $response['body']['smtpSecure']);
/** Test Reading Project */
// Check the project
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -651,12 +920,32 @@ class ProjectsConsoleClientTest extends Scope
$this->assertTrue($response['body']['smtpEnabled']);
$this->assertEquals('mailer@appwrite.io', $response['body']['smtpSenderEmail']);
$this->assertEquals('Mailer', $response['body']['smtpSenderName']);
$this->assertEquals('maildev', $response['body']['smtpHost']);
$this->assertEquals(1025, $response['body']['smtpPort']);
$this->assertEquals('user', $response['body']['smtpUsername']);
$this->assertEquals('password', $response['body']['smtpPassword']);
$this->assertEquals($smtpHost, $response['body']['smtpHost']);
$this->assertEquals($smtpPort, $response['body']['smtpPort']);
$this->assertEquals($smtpUsername, $response['body']['smtpUsername']);
$this->assertEquals($smtpPassword, $response['body']['smtpPassword']);
$this->assertEquals('', $response['body']['smtpSecure']);
/**
* Test for FAILURE: Invalid Credentials
*/
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'enabled' => true,
'senderEmail' => 'fail@appwrite.io',
'senderName' => 'Failing Mailer',
'host' => $smtpHost,
'port' => $smtpPort,
'username' => 'invalid-user',
'password' => 'bad-password',
]);
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertEquals(Exception::PROJECT_SMTP_CONFIG_INVALID, $response['body']['type']);
$this->assertStringContainsStringIgnoringCase('Could not authenticate', $response['body']['message']);
return $data;
}
@@ -665,6 +954,11 @@ class ProjectsConsoleClientTest extends Scope
*/
public function testCreateProjectSMTPTests(): void
{
$smtpHost = System::getEnv('_APP_SMTP_HOST', "maildev");
$smtpPort = intval(System::getEnv('_APP_SMTP_PORT', "1025"));
$smtpUsername = System::getEnv('_APP_SMTP_USERNAME', 'user');
$smtpPassword = System::getEnv('_APP_SMTP_PASSWORD', 'password');
// Create a team
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
@@ -699,10 +993,10 @@ class ProjectsConsoleClientTest extends Scope
'senderEmail' => 'custommailer@appwrite.io',
'senderName' => 'Custom Mailer',
'replyTo' => 'reply@appwrite.io',
'host' => 'maildev',
'port' => 1025,
'username' => '',
'password' => '',
'host' => $smtpHost,
'port' => $smtpPort,
'username' => $smtpUsername,
'password' => $smtpPassword,
]);
$this->assertEquals(204, $response['headers']['status-code']);
@@ -736,10 +1030,10 @@ class ProjectsConsoleClientTest extends Scope
'senderEmail' => 'custommailer@appwrite.io',
'senderName' => 'Custom Mailer',
'replyTo' => 'reply@appwrite.io',
'host' => 'maildev',
'port' => 1025,
'username' => '',
'password' => '',
'host' => $smtpHost,
'port' => $smtpPort,
'username' => $smtpUsername,
'password' => $smtpPassword,
]);
$this->assertEquals(204, $response['headers']['status-code']);
@@ -752,10 +1046,10 @@ class ProjectsConsoleClientTest extends Scope
'senderEmail' => 'custommailer@appwrite.io',
'senderName' => 'Custom Mailer',
'replyTo' => 'reply@appwrite.io',
'host' => 'maildev',
'port' => 1025,
'username' => '',
'password' => '',
'host' => $smtpHost,
'port' => $smtpPort,
'username' => $smtpUsername,
'password' => $smtpPassword,
]);
$this->assertEquals(400, $response['headers']['status-code']);
@@ -776,7 +1070,7 @@ class ProjectsConsoleClientTest extends Scope
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('Account Verification', $response['body']['subject']);
$this->assertEquals('Account Verification for {{project}}', $response['body']['subject']);
$this->assertEquals('', $response['body']['senderEmail']);
$this->assertEquals('verification', $response['body']['type']);
$this->assertEquals('en-us', $response['body']['locale']);
@@ -879,7 +1173,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals('Project Test 2', $response['body']['name']);
$this->assertEquals('Project Test', $response['body']['name']);
$this->assertArrayHasKey('platforms', $response['body']);
$this->assertArrayHasKey('webhooks', $response['body']);
$this->assertArrayHasKey('keys', $response['body']);
@@ -959,39 +1253,6 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(15, $response['body']['authDuration']);
// Create session
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
]), [
'email' => $userEmail,
'password' => 'password',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$sessionCookie = $response['headers']['set-cookie'];
// Wait 10 seconds, ensure valid session, extend session
\sleep(10);
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'Cookie' => $sessionCookie,
]));
$this->assertEquals(200, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_PATCH, '/account/sessions/current', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => $sessionCookie,
]));
$this->assertEquals(200, $response['headers']['status-code']);
// Wait 20 seconds, ensure non-valid session
\sleep(20);
@@ -3132,7 +3393,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('users.write', $response['body']['scopes']);
$this->assertContains('collections.read', $response['body']['scopes']);
$this->assertContains('tables.read', $response['body']['scopes']);
$this->assertCount(3, $response['body']['scopes']);
$this->assertCount(4, $response['body']['scopes']);
$this->assertArrayHasKey('sdks', $response['body']);
$this->assertEmpty($response['body']['sdks']);
$this->assertArrayHasKey('accessedAt', $response['body']);
@@ -3151,7 +3412,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('users.write', $response['body']['scopes']);
$this->assertContains('collections.read', $response['body']['scopes']);
$this->assertContains('tables.read', $response['body']['scopes']);
$this->assertCount(3, $response['body']['scopes']);
$this->assertCount(4, $response['body']['scopes']);
$this->assertArrayHasKey('sdks', $response['body']);
$this->assertEmpty($response['body']['sdks']);
$this->assertArrayHasKey('accessedAt', $response['body']);
@@ -5086,8 +5347,8 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEmpty($response['body']);
/**
* Get rate limit trying to use the deleted key
*/
* Get rate limit trying to use the deleted key
*/
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,