diff --git a/app/config/platforms.php b/app/config/platforms.php index edb94f1f96..34c0290832 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -60,7 +60,7 @@ return [ [ 'key' => 'flutter', 'name' => 'Flutter', - 'version' => '20.2.1', + 'version' => '20.2.2', 'url' => 'https://github.com/appwrite/sdk-for-flutter', 'package' => 'https://pub.dev/packages/appwrite', 'enabled' => true, @@ -79,7 +79,7 @@ return [ [ 'key' => 'apple', 'name' => 'Apple', - 'version' => '13.3.0', + 'version' => '13.3.1', 'url' => 'https://github.com/appwrite/sdk-for-apple', 'package' => 'https://github.com/appwrite/sdk-for-apple', 'enabled' => true, @@ -226,7 +226,7 @@ return [ [ 'key' => 'cli', 'name' => 'Command Line', - 'version' => '10.2.3', + 'version' => '11.0.0', 'url' => 'https://github.com/appwrite/sdk-for-cli', 'package' => 'https://www.npmjs.com/package/appwrite-cli', 'enabled' => true, @@ -300,7 +300,7 @@ return [ [ 'key' => 'python', 'name' => 'Python', - 'version' => '13.4.1', + 'version' => '13.5.0', 'url' => 'https://github.com/appwrite/sdk-for-python', 'package' => 'https://pypi.org/project/appwrite/', 'enabled' => true, diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 47557e856e..9d1987591e 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -75,6 +75,14 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, Doc $subject = $locale->getText("emails.sessionAlert.subject"); $preview = $locale->getText("emails.sessionAlert.preview"); $customTemplate = $project->getAttribute('templates', [])['email.sessionAlert-' . $locale->default] ?? []; + $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); + + $validator = new FileName(); + if (!$validator->isValid($smtpBaseTemplate)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); + } + + $bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl'; $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-session-alert.tpl'); $message @@ -157,12 +165,25 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, Doc 'country' => $locale->getText('countries.' . $session->getAttribute('countryCode'), $locale->getText('locale.country.unknown')), ]; + if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { + $emailVariables = array_merge($emailVariables, [ + 'accentColor' => APP_EMAIL_ACCENT_COLOR, + 'logoUrl' => APP_EMAIL_LOGO_URL, + 'twitterUrl' => APP_SOCIAL_TWITTER, + 'discordUrl' => APP_SOCIAL_DISCORD, + 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, + 'termsUrl' => APP_EMAIL_TERMS_URL, + 'privacyUrl' => APP_EMAIL_PRIVACY_URL, + ]); + } + $email = $user->getAttribute('email'); $queueForMails ->setSubject($subject) ->setPreview($preview) ->setBody($body) + ->setBodyTemplate($bodyTemplate) ->setVariables($emailVariables) ->setRecipient($email) ->trigger(); diff --git a/docs/examples/1.8.x/console-cli/examples/migrations/create-csv-export.md b/docs/examples/1.8.x/console-cli/examples/migrations/create-csv-export.md new file mode 100644 index 0000000000..e56afae786 --- /dev/null +++ b/docs/examples/1.8.x/console-cli/examples/migrations/create-csv-export.md @@ -0,0 +1,4 @@ +appwrite migrations create-csv-export \ + --resource-id \ + --bucket-id \ + --filename diff --git a/docs/examples/1.8.x/console-cli/examples/migrations/create-csv-import.md b/docs/examples/1.8.x/console-cli/examples/migrations/create-csv-import.md new file mode 100644 index 0000000000..196112bdf8 --- /dev/null +++ b/docs/examples/1.8.x/console-cli/examples/migrations/create-csv-import.md @@ -0,0 +1,4 @@ +appwrite migrations create-csv-import \ + --bucket-id \ + --file-id \ + --resource-id diff --git a/docs/examples/1.8.x/console-web/examples/migrations/create-csv-export.md b/docs/examples/1.8.x/console-web/examples/migrations/create-csv-export.md new file mode 100644 index 0000000000..e1b909a852 --- /dev/null +++ b/docs/examples/1.8.x/console-web/examples/migrations/create-csv-export.md @@ -0,0 +1,22 @@ +import { Client, Migrations } from "@appwrite.io/console"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const migrations = new Migrations(client); + +const result = await migrations.createCSVExport({ + resourceId: '', + bucketId: '', + filename: '', + columns: [], // optional + queries: [], // optional + delimiter: '', // optional + enclosure: '', // optional + escape: '', // optional + header: false, // optional + notify: false // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/console-web/examples/migrations/create-csv-import.md b/docs/examples/1.8.x/console-web/examples/migrations/create-csv-import.md new file mode 100644 index 0000000000..9b8b2b2b33 --- /dev/null +++ b/docs/examples/1.8.x/console-web/examples/migrations/create-csv-import.md @@ -0,0 +1,16 @@ +import { Client, Migrations } from "@appwrite.io/console"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const migrations = new Migrations(client); + +const result = await migrations.createCSVImport({ + bucketId: '', + fileId: '', + resourceId: '', + internalFile: false // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/create-collection.md b/docs/examples/1.8.x/server-nodejs/examples/databases/create-collection.md index 8ad770d907..9bc014b59b 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/create-collection.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/create-collection.md @@ -11,7 +11,7 @@ const result = await databases.createCollection({ databaseId: '', collectionId: '', name: '', - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional documentSecurity: false, // optional enabled: false // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/create-document.md b/docs/examples/1.8.x/server-nodejs/examples/databases/create-document.md index 6fe77c42be..e6b9b49553 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/create-document.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/create-document.md @@ -18,6 +18,6 @@ const result = await databases.createDocument({ "age": 30, "isAdmin": false }, - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/update-collection.md b/docs/examples/1.8.x/server-nodejs/examples/databases/update-collection.md index d0d25b74d6..4cdc3a203b 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/update-collection.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/update-collection.md @@ -11,7 +11,7 @@ const result = await databases.updateCollection({ databaseId: '', collectionId: '', name: '', - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional documentSecurity: false, // optional enabled: false // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/update-document.md b/docs/examples/1.8.x/server-nodejs/examples/databases/update-document.md index 3e953760a1..d33d78d7d3 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/update-document.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/update-document.md @@ -12,6 +12,6 @@ const result = await databases.updateDocument({ collectionId: '', documentId: '', data: {}, // optional - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-document.md b/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-document.md index 0aaec4e6cb..8fe4b35194 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-document.md +++ b/docs/examples/1.8.x/server-nodejs/examples/databases/upsert-document.md @@ -12,6 +12,6 @@ const result = await databases.upsertDocument({ collectionId: '', documentId: '', data: {}, - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/storage/create-bucket.md b/docs/examples/1.8.x/server-nodejs/examples/storage/create-bucket.md index f1f029491a..b47d2c8353 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/storage/create-bucket.md +++ b/docs/examples/1.8.x/server-nodejs/examples/storage/create-bucket.md @@ -10,7 +10,7 @@ const storage = new sdk.Storage(client); const result = await storage.createBucket({ bucketId: '', name: '', - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional fileSecurity: false, // optional enabled: false, // optional maximumFileSize: 1, // optional diff --git a/docs/examples/1.8.x/server-nodejs/examples/storage/create-file.md b/docs/examples/1.8.x/server-nodejs/examples/storage/create-file.md index 628faf7249..8dc1745585 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/storage/create-file.md +++ b/docs/examples/1.8.x/server-nodejs/examples/storage/create-file.md @@ -12,5 +12,5 @@ const result = await storage.createFile({ bucketId: '', fileId: '', file: InputFile.fromPath('/path/to/file', 'filename'), - permissions: ["read("any")"] // optional + permissions: [sdk.Permission.read(sdk.Role.any())] // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/storage/update-bucket.md b/docs/examples/1.8.x/server-nodejs/examples/storage/update-bucket.md index 136ebafe1b..9535914eeb 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/storage/update-bucket.md +++ b/docs/examples/1.8.x/server-nodejs/examples/storage/update-bucket.md @@ -10,7 +10,7 @@ const storage = new sdk.Storage(client); const result = await storage.updateBucket({ bucketId: '', name: '', - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional fileSecurity: false, // optional enabled: false, // optional maximumFileSize: 1, // optional diff --git a/docs/examples/1.8.x/server-nodejs/examples/storage/update-file.md b/docs/examples/1.8.x/server-nodejs/examples/storage/update-file.md index 2d78d5fb91..131682134d 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/storage/update-file.md +++ b/docs/examples/1.8.x/server-nodejs/examples/storage/update-file.md @@ -11,5 +11,5 @@ const result = await storage.updateFile({ bucketId: '', fileId: '', name: '', // optional - permissions: ["read("any")"] // optional + permissions: [sdk.Permission.read(sdk.Role.any())] // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-row.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-row.md index 4468c168e8..d437501ba0 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-row.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-row.md @@ -18,6 +18,6 @@ const result = await tablesDB.createRow({ "age": 30, "isAdmin": false }, - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-table.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-table.md index 1b252f1484..6a4c12d34d 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-table.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/create-table.md @@ -11,7 +11,7 @@ const result = await tablesDB.createTable({ databaseId: '', tableId: '', name: '', - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional rowSecurity: false, // optional enabled: false // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-row.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-row.md index 58583af745..d5d2ee3002 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-row.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-row.md @@ -12,6 +12,6 @@ const result = await tablesDB.updateRow({ tableId: '', rowId: '', data: {}, // optional - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional transactionId: '' // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-table.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-table.md index b61fd6ac4e..97483daa03 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-table.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/update-table.md @@ -11,7 +11,7 @@ const result = await tablesDB.updateTable({ databaseId: '', tableId: '', name: '', - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional rowSecurity: false, // optional enabled: false // optional }); diff --git a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-row.md b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-row.md index bfb833356a..f48b0daebd 100644 --- a/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-row.md +++ b/docs/examples/1.8.x/server-nodejs/examples/tablesdb/upsert-row.md @@ -12,6 +12,6 @@ const result = await tablesDB.upsertRow({ tableId: '', rowId: '', data: {}, // optional - permissions: ["read("any")"], // optional + permissions: [sdk.Permission.read(sdk.Role.any())], // optional transactionId: '' // optional }); diff --git a/docs/sdks/apple/CHANGELOG.md b/docs/sdks/apple/CHANGELOG.md index 9ffa37cdf8..df3170cc4d 100644 --- a/docs/sdks/apple/CHANGELOG.md +++ b/docs/sdks/apple/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 13.3.1 + +* Fix `onOpen` callback not being called when the websocket connection is established +* Fix add missing `scheduled` value to `ExecutionStatus` enum + ## 13.3.0 * Add `onOpen`, `onClose` and `onError` callbacks to `Realtime` service diff --git a/docs/sdks/cli/CHANGELOG.md b/docs/sdks/cli/CHANGELOG.md index 8a1ea0f360..0ffcb91b80 100644 --- a/docs/sdks/cli/CHANGELOG.md +++ b/docs/sdks/cli/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## 11.0.0 + +* Rename `create-csv-migration` to `create-csv-import` command to create a CSV import of a collection/table +* Add `create-csv-export` command to create a CSV export of a collection/table +* Add `create-resend-provider` and `update-resend-provider` commands to create and update a Resend Email provider +* Fix syncing of tables deleted locally during `push tables` command +* Fix added push command support for cli spatial types +* Fix attribute changing during push +* Replace pkg with @yao-pkg/pkg in dependencies + ## 10.2.3 * Fix `init tables` command not working diff --git a/docs/sdks/flutter/CHANGELOG.md b/docs/sdks/flutter/CHANGELOG.md index 7ac74d0c05..4c723b8017 100644 --- a/docs/sdks/flutter/CHANGELOG.md +++ b/docs/sdks/flutter/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 20.2.2 + +* Widen `device_info_plus` and `package_info_plus` dependencies to allow for newer versions for Android 15+ support +* Fix `CHUNK_SIZE` constant to `chunkSize` +* Fix missing `@override` annotation to `toMap` method in all model classes + ## 20.2.1 * Add transaction support for Databases and TablesDB diff --git a/docs/sdks/python/CHANGELOG.md b/docs/sdks/python/CHANGELOG.md index 7d8327b919..cb7f47d379 100644 --- a/docs/sdks/python/CHANGELOG.md +++ b/docs/sdks/python/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 13.5.0 + +* Add `create_resend_provider` and `update_resend_provider` methods to `Messaging` service +* Improve deprecation warnings +* Fix adding `Optional[]` to optional parameters +* Fix passing of `None` to nullable parameters + ## 13.4.1 * Add transaction support for Databases and TablesDB diff --git a/docs/tutorials/release-sdks.md b/docs/tutorials/release-sdks.md new file mode 100644 index 0000000000..99c0fa4fd3 --- /dev/null +++ b/docs/tutorials/release-sdks.md @@ -0,0 +1,186 @@ +# Releasing Appwrite SDKs + +This document is part of the Appwrite contributors' guide. Before you continue reading this document, make sure you have read the [Code of Conduct](https://github.com/appwrite/.github/blob/main/CODE_OF_CONDUCT.md) and the [Contributing Guide](https://github.com/appwrite/appwrite/blob/master/CONTRIBUTING.md). + +## Getting Started + +### Agenda + +This tutorial will cover how to properly release one or multiple Appwrite SDKs. The SDK release process involves updating the SDK generator, configuring Docker secrets, and running the release script. + +### Prerequisites + +Before releasing SDKs, you need to: + +1. **Release a new SDK generator version** - Create a PR in the [sdk-generator](https://github.com/appwrite/sdk-generator) repository with your respective sdk's changes. Wait for the PR to get merged and be released. + +2. **Update the SDK generator dependency** + - Update composer dependencies to use the new SDK generator version: + ```bash + docker run --rm --interactive --tty --volume "$(pwd)":/app composer update --ignore-platform-reqs --optimize-autoloader --no-scripts + ``` + + - Verify that `composer.lock` reflects the new SDK generator version + +### Configure Docker Secrets + +To enable SDK releases via GitHub, you need to mount SSH keys and configure GitHub authentication in your Docker environment. + +#### Update Dockerfile + +Add the following configuration to your `Dockerfile`: + +```dockerfile +ARG GH_TOKEN +ENV GH_TOKEN=your_github_token_here +RUN git config --global user.email "your-email@example.com" +RUN apk add --update --no-cache openssh-client github-cli +``` + +Replace: +- `your_github_token_here` with your GitHub personal access token (with appropriate permissions) +- `your-email@example.com` with your Git email address + +#### Update docker-compose.yml + +Add the SSH key volume mount to the `appwrite` service in `docker-compose.yml`: + +```yaml +services: + appwrite: + volumes: + - ~/.ssh:/root/.ssh + # ... other volumes +``` + +This mounts your SSH keys from the host machine, allowing the container to authenticate with GitHub. + +### Updating Specs + +The SDK generator script heavily relies on API specification files (specs). Whenever you are adding a new endpoint, updating parameters, or making any API changes, you need to update the specs. + +Generate specs for the latest version: + +```bash +docker compose exec appwrite specs +``` + +Also generate specs for the current stable Appwrite version: + +```bash +docker compose exec appwrite specs --version=1.8.x +``` + +### Running the SDK Release Script + +Before running the SDK release script, ensure you update the following for each SDK you plan to release: + +1. **Update the changelog** - Add release notes to the SDK's `CHANGELOG.md` file (located in `docs/sdks//CHANGELOG.md`) +2. **Bump the version** - Update the version number (patch, minor, or major) in `app/config/platforms.php` + +Once you have completed these updates, run the SDK release script: + +```bash +docker compose exec appwrite sdks +``` + +The script will prompt you for: +1. **Platform** - Select client, server, console, or `*` for all platforms +2. **SDK(s)** - Choose specific SDK(s) or `*` for all +3. **Appwrite version** - Specify the version (e.g., `1.8.x`) +4. **Git options** - Configure push settings and PR creation + +#### Releasing Multiple SDKs + +If you are releasing multiple SDKs across different platforms, you can specify them directly: + +```bash +docker compose exec appwrite sdks --sdks=dart,flutter,cli,python +``` + +#### Pull Request Summary + +After the script completes, you'll receive a summary of created pull requests: + +```text +Pull Request Summary +Dart: https://github.com/appwrite/sdk-for-dart/pull/123 +Flutter: https://github.com/appwrite/sdk-for-flutter/pull/124 +CLI: https://github.com/appwrite/sdk-for-cli/pull/125 +``` + +### Creating GitHub Releases + +> **Note:** This section is for Appwrite maintainers only. + +After the PRs have been reviewed and merged by an Appwrite Lead, you can create GitHub releases automatically. + +#### Dry Run + +First, perform a dry run to preview the releases: + +```bash +docker compose exec appwrite sdks --release=yes +``` + +This will display what releases would be created: + +```text +[DRY RUN] Would create release for Dart SDK: + Repository: appwrite/sdk-for-dart + Version: 13.0.0 + Title: 13.0.0 + Target Branch: main + Previous Version: 12.0.2 + Release Notes: + ## What's Changed + - Added support for new Users API endpoints + - Fixed authentication token handling + - Updated dependencies +``` + +#### Execute Release + +After verifying the dry run output, create the actual releases: + +```bash +docker compose exec appwrite sdks --release=yes --commit=yes +``` + +## Reference + +### Configuration Files + +SDK configurations are defined in the following files: + +- **`app/config/platforms.php`** - Platform and SDK definitions, including metadata, Git repository URLs, versions, and enabled/disabled status +- **`src/Appwrite/Platform/Tasks/SDKs.php`** - SDK generation and release logic +- **`docs/sdks//CHANGELOG.md`** - Changelog files for each SDK + +## Troubleshooting + +### Authentication Issues + +If you encounter authentication problems: +- **GitHub token** - Verify your token has the correct permissions (repo access, workflow permissions) +- **SSH keys** - Ensure your SSH keys are properly configured in `~/.ssh/` and added to your GitHub account +- **Git configuration** - Check that the Git email in the Dockerfile matches your GitHub account + +### Common Issues + +- **"Release already exists"** - The script automatically skips releases that already exist for the specified version +- **"No changes detected"** - Ensure you've updated the specs and that there are actual API changes to generate +- **Permission denied** - Verify that your GitHub token and SSH keys have write access to the SDK repositories + +## Summary + +Congrats! You've successfully learned how to release Appwrite SDKs. Remember to: + +1. Update SDK generator and run `composer update` +2. Configure Docker secrets (GitHub token and SSH keys) +3. Update specs for both latest and stable versions +4. Update changelogs and bump versions in `platforms.php` +5. Run the SDK script and create PRs +6. (Maintainers only) Create GitHub releases after PR approval + +Happy releasing! 🎉 diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 2fb15c5f7d..f587e0f946 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -48,13 +48,18 @@ class SDKs extends Action ->param('message', null, new Nullable(new Text(256)), 'Commit Message', optional: true) ->param('release', null, new Nullable(new WhiteList(['yes', 'no'])), 'Should we create releases?', optional: true) ->param('commit', null, new Nullable(new WhiteList(['yes', 'no'])), 'Actually create releases (yes) or dry-run (no)?', optional: true) + ->param('sdks', null, new Nullable(new Text(256)), 'Selected SDKs', optional: true) ->callback($this->action(...)); } - public function action(?string $selectedPlatform, ?string $selectedSDK, ?string $version, ?string $git, ?string $production, ?string $message, ?string $release, ?string $commit): void + public function action(?string $selectedPlatform, ?string $selectedSDK, ?string $version, ?string $git, ?string $production, ?string $message, ?string $release, ?string $commit, ?string $sdks): void { - $selectedPlatform ??= Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):'); - $selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):')); + if (!$sdks) { + $selectedPlatform ??= Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):'); + $selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):')); + } else { + $sdks = explode(',', $sdks); + } $version ??= Console::confirm('Choose an Appwrite version'); $createRelease = ($release === 'yes'); @@ -104,12 +109,12 @@ class SDKs extends Action $platforms = Config::getParam('platforms'); foreach ($platforms as $key => $platform) { - if ($selectedPlatform !== $key && $selectedPlatform !== '*') { + if ($selectedPlatform !== $key && $selectedPlatform !== '*' && ($sdks === null)) { continue; } foreach ($platform['sdks'] as $language) { - if ($selectedSDK !== $language['key'] && $selectedSDK !== '*') { + if ($selectedSDK !== $language['key'] && $selectedSDK !== '*' && ($sdks === null || !\in_array($language['key'], $sdks))) { continue; } @@ -472,38 +477,60 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $errorMessage = implode("\n", $prOutput); if (strpos($errorMessage, 'already exists') !== false) { Console::warning("Pull request already exists for {$language['name']} SDK, updating title and body..."); - - $updateCommand = 'cd ' . $target . ' && \ - gh pr edit "' . $gitBranch . '" \ + $prNumberCommand = 'cd ' . $target . ' && \ + gh pr list \ --repo "' . $repoName . '" \ - --title "' . $prTitle . '" \ - --body "' . $prBody . '" \ + --head "' . $gitBranch . '" \ + --json number \ + --jq ".[0].number" \ 2>&1'; - $updateOutput = []; - $updateReturnCode = 0; - \exec($updateCommand, $updateOutput, $updateReturnCode); + $prNumberOutput = []; + $prNumberReturnCode = 0; + \exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode); - if ($updateReturnCode === 0) { - Console::success("Successfully updated pull request for {$language['name']} SDK"); + if ($prNumberReturnCode === 0 && !empty($prNumberOutput[0])) { + $prNumber = trim($prNumberOutput[0]); - $prUrlCommand = 'cd ' . $target . ' && \ - gh pr view "' . $gitBranch . '" \ - --repo "' . $repoName . '" \ - --json url \ - --jq .url \ + // Use API directly to update PR to avoid deprecated projectCards field + $updateCommand = 'cd ' . $target . ' && \ + gh api \ + --method PATCH \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/' . $repoName . '/pulls/' . $prNumber . ' \ + -f title="' . $prTitle . '" \ + -f body="' . $prBody . '" \ 2>&1'; - $prUrlOutput = []; - $prUrlReturnCode = 0; - \exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode); + $updateOutput = []; + $updateReturnCode = 0; + \exec($updateCommand, $updateOutput, $updateReturnCode); - if ($prUrlReturnCode === 0 && !empty($prUrlOutput)) { - $prUrls[$language['name']] = $prUrlOutput[0]; + if ($updateReturnCode === 0) { + Console::success("Successfully updated pull request for {$language['name']} SDK"); + + $prUrlCommand = 'cd ' . $target . ' && \ + gh pr list \ + --repo "' . $repoName . '" \ + --head "' . $gitBranch . '" \ + --json url \ + --jq ".[0].url" \ + 2>&1'; + + $prUrlOutput = []; + $prUrlReturnCode = 0; + \exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode); + + if ($prUrlReturnCode === 0 && !empty($prUrlOutput)) { + $prUrls[$language['name']] = trim($prUrlOutput[0]); + } + } else { + $updateErrorMessage = implode("\n", $updateOutput); + Console::error("Failed to update pull request for {$language['name']} SDK: " . $updateErrorMessage); } } else { - $updateErrorMessage = implode("\n", $updateOutput); - Console::error("Failed to update pull request for {$language['name']} SDK: " . $updateErrorMessage); + Console::error("Failed to get PR number for {$language['name']} SDK"); } } else { Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage); diff --git a/tests/e2e/Services/Account/AccountConsoleClientTest.php b/tests/e2e/Services/Account/AccountConsoleClientTest.php index 1df9ef6c18..3e43d443e3 100644 --- a/tests/e2e/Services/Account/AccountConsoleClientTest.php +++ b/tests/e2e/Services/Account/AccountConsoleClientTest.php @@ -88,4 +88,112 @@ class AccountConsoleClientTest extends Scope $this->assertEquals($response['headers']['status-code'], 204); } + + public function testSessionAlert(): void + { + $email = uniqid() . 'session-alert@appwrite.io'; + $password = 'password123'; + $name = 'Session Alert Tester'; + + // Create a new account + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-dev-key' => $this->getProject()['devKey'] ?? '' + ]), [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => $name, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Create first session for the new account + $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'], + 'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // Create second session for the new account + $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'], + 'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + + // Check the alert email + $lastEmail = $this->getLastEmail(); + + $this->assertEquals($email, $lastEmail['to'][0]['address']); + $this->assertStringContainsString('Security alert: new session', $lastEmail['subject']); + $this->assertStringContainsString($response['body']['ip'], $lastEmail['text']); // IP Address + $this->assertStringContainsString('Unknown', $lastEmail['text']); // Country + $this->assertStringContainsString($response['body']['clientName'], $lastEmail['text']); // Client name + $this->assertStringContainsStringIgnoringCase('Appwrite logo', $lastEmail['html']); + + // Verify no alert sent in OTP login + $response = $this->client->call(Client::METHOD_POST, '/account/tokens/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => ID::unique(), + 'email' => 'otpuser2@appwrite.io' + ]); + + $this->assertEquals($response['headers']['status-code'], 201); + $this->assertNotEmpty($response['body']['$id']); + $this->assertNotEmpty($response['body']['$createdAt']); + $this->assertNotEmpty($response['body']['userId']); + $this->assertNotEmpty($response['body']['expire']); + $this->assertEmpty($response['body']['secret']); + $this->assertEmpty($response['body']['phrase']); + $this->assertStringContainsStringIgnoringCase('New login detected on '. $this->getProject()['name'], $lastEmail['text']); + + $userId = $response['body']['userId']; + + $lastEmail = $this->getLastEmail(); + + $this->assertEquals('otpuser2@appwrite.io', $lastEmail['to'][0]['address']); + $this->assertEquals('OTP for ' . $this->getProject()['name'] . ' Login', $lastEmail['subject']); + + // Find 6 concurrent digits in email text - OTP + preg_match_all("/\b\d{6}\b/", $lastEmail['text'], $matches); + $code = ($matches[0] ?? [])[0] ?? ''; + + $this->assertNotEmpty($code); + + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/token', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => $userId, + 'secret' => $code + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals($userId, $response['body']['userId']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertNotEmpty($response['body']['expire']); + $this->assertEmpty($response['body']['secret']); + + $lastEmailId = $lastEmail['id']; + $lastEmail = $this->getLastEmail(); + $this->assertEquals($lastEmailId, $lastEmail['id']); + } } diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 8127b8a1cd..0163f1b842 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -1368,10 +1368,7 @@ class AccountCustomClientTest extends Scope return $data; } - /** - * @depends testCreateAccountSession - */ - public function testSessionAlert($data): void + public function testSessionAlert(): void { $email = uniqid() . 'session-alert@appwrite.io'; $password = 'password123'; @@ -1437,6 +1434,7 @@ class AccountCustomClientTest extends Scope $this->assertStringContainsString($response['body']['ip'], $lastEmail['text']); // IP Address $this->assertStringContainsString('Unknown', $lastEmail['text']); // Country $this->assertStringContainsString($response['body']['clientName'], $lastEmail['text']); // Client name + $this->assertStringNotContainsStringIgnoringCase('Appwrite logo', $lastEmail['html']); // Verify no alert sent in OTP login $response = $this->client->call(Client::METHOD_POST, '/account/tokens/email', array_merge([