mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.9.x' into chore/bump-image
This commit is contained in:
@@ -1,107 +1,120 @@
|
||||
# AGENTS.md
|
||||
# Appwrite
|
||||
|
||||
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.
|
||||
Self-hosted Backend-as-a-Service platform. Hybrid monolithic-microservice architecture built with PHP 8.3+ on Swoole, delivered as Docker containers.
|
||||
|
||||
## Project Overview
|
||||
## Commands
|
||||
|
||||
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.
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `docker compose up -d --force-recreate --build` | Build and start all services |
|
||||
| `docker compose exec appwrite test tests/e2e/Services/[Service]` | Run E2E tests for a service |
|
||||
| `docker compose exec appwrite test tests/e2e/Services/[Service] --filter=[Method]` | Run a single test method |
|
||||
| `docker compose exec appwrite test tests/unit/` | Run unit tests |
|
||||
| `composer format` | Auto-format code (Pint, PSR-12) |
|
||||
| `composer format <file>` | Format a specific file |
|
||||
| `composer lint <file>` | Check formatting of a file |
|
||||
| `composer analyze` | Static analysis (PHPStan level 3) |
|
||||
| `composer check` | Same as `analyze` |
|
||||
|
||||
**Key Technologies:**
|
||||
- **Backend:** PHP 8.3+, Swoole
|
||||
- **Libraries:** Utopia PHP
|
||||
- **Database:** MariaDB, Redis
|
||||
- **Cache:** Redis
|
||||
- **Queue:** Redis
|
||||
- **Containers:** Docker
|
||||
## Stack
|
||||
|
||||
## Development Commands
|
||||
- PHP 8.3+, Swoole 6.x (async runtime, replaces PHP-FPM)
|
||||
- Utopia PHP framework (HTTP routing, CLI, DI, queue)
|
||||
- MongoDB (default), MariaDB, MySQL, PostgreSQL (adapters via utopia-php/database)
|
||||
- Redis (cache, queue, pub/sub)
|
||||
- Docker + Traefik (reverse proxy)
|
||||
- PHPUnit 12, Pint (PSR-12), PHPStan level 3
|
||||
|
||||
```bash
|
||||
# Run Appwrite
|
||||
docker compose up -d --force-recreate --build
|
||||
## Project layout
|
||||
|
||||
# Run specific test
|
||||
docker compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName] --filter=[FunctionName]
|
||||
- **src/Appwrite/Platform/Modules/** -- feature modules (Account, Avatars, Compute, Console, Databases, Functions, Health, Project, Projects, Proxy, Sites, Storage, Teams, Tokens, VCS, Webhooks)
|
||||
- **src/Appwrite/Platform/Workers/** -- background job workers
|
||||
- **src/Appwrite/Platform/Tasks/** -- CLI tasks
|
||||
- **app/init.php** -- bootstrap (registers services, resources, listeners)
|
||||
- **app/init/** -- configs, constants, locales, models, registers, resources, span, database filters/formats
|
||||
- **bin/** -- CLI entry points: `worker-*` (14 workers), `schedule-*`, `queue-*`, plus `doctor`, `install`, `migrate`, `realtime`, `upgrade`, `ssl`, `vars`, `maintenance`, `interval`, `specs`, `sdks`, etc.
|
||||
- **tests/e2e/** -- end-to-end tests per service
|
||||
- **tests/unit/** -- unit tests
|
||||
- **public/** -- static assets and generated SDKs
|
||||
|
||||
# Format code
|
||||
composer format
|
||||
## Module structure
|
||||
|
||||
Each module under `src/Appwrite/Platform/Modules/{Name}/` contains:
|
||||
|
||||
```
|
||||
Module.php -- registers all services for the module
|
||||
Services/Http.php -- registers HTTP endpoints
|
||||
Services/Workers.php -- registers background workers
|
||||
Services/Tasks.php -- registers CLI tasks
|
||||
Http/{Service}/ -- endpoint actions (Create.php, Get.php, Update.php, Delete.php, XList.php)
|
||||
Workers/ -- worker implementations
|
||||
Tasks/ -- CLI task implementations
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
HTTP endpoint nesting reflects the URL path. Sub-resources get subdirectories. For example, within the Functions module:
|
||||
`Http/Deployments/Template/Create.php` -> `POST /v1/functions/:functionId/deployments/template`
|
||||
|
||||
- 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
|
||||
File names in Http directories must only be `Get.php`, `Create.php`, `Update.php`, `Delete.php`, or `XList.php`. For non-CRUD operations, model the endpoint as a property update. For example, updating a team membership status lives at `Teams/Http/Memberships/Status/Update.php` (`PATCH /v1/teams/:teamId/memberships/:membershipId/status`).
|
||||
|
||||
### Naming Conventions
|
||||
Register new modules in `src/Appwrite/Platform/Appwrite.php`. Detailed module guide: `src/Appwrite/Platform/AGENTS.md`.
|
||||
|
||||
#### `resourceType` Naming Rule
|
||||
## Action pattern (HTTP endpoints)
|
||||
|
||||
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'
|
||||
class Create extends Action
|
||||
{
|
||||
public static function getName(): string { return 'createTeam'; }
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/teams')
|
||||
->desc('Create team')
|
||||
->groups(['api', 'teams'])
|
||||
->label('event', 'teams.[teamId].create')
|
||||
->label('scope', 'teams.write')
|
||||
->param('teamId', '', new CustomId(), 'Team ID.')
|
||||
->param('name', null, new Text(128), 'Team name.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $teamId,
|
||||
string $name,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Patterns
|
||||
Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `$user`, `$project`, `$queueForEvents`, `$queueForMails`, `$queueForDeletes`.
|
||||
|
||||
### Document Update Optimization
|
||||
## Conventions
|
||||
|
||||
When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`.
|
||||
- PSR-12 formatting enforced by Pint. PSR-4 autoloading.
|
||||
- `resourceType` values are always **plural**: `'functions'`, `'sites'`, `'deployments'`.
|
||||
- When updating documents, pass only changed attributes as a sparse Document:
|
||||
```php
|
||||
// correct
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $name,
|
||||
]));
|
||||
// incorrect -- passing full document is inefficient
|
||||
$user->setAttribute('name', $name);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
```
|
||||
Exceptions: migrations, `array_merge()` with `getArrayCopy()`, updates where nearly all attributes change, complex nested relationship logic requiring full document state.
|
||||
- Avoid introducing dependencies outside the `utopia-php` ecosystem.
|
||||
- Never hardcode credentials -- use environment variables.
|
||||
- Code changes may require container restart. No central log location -- check relevant containers.
|
||||
|
||||
**Correct Pattern:**
|
||||
```php
|
||||
// Good: Pass only changed attributes directly
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
]));
|
||||
```
|
||||
## Cross-repo context
|
||||
|
||||
**Incorrect Pattern:**
|
||||
```php
|
||||
$user->setAttribute('name', $name);
|
||||
$user->setAttribute('email', $email);
|
||||
|
||||
// Bad: Passing full document is inefficient
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
```
|
||||
|
||||
**Exceptions:**
|
||||
- Migration files (need full document updates by design)
|
||||
- Cases already using `array_merge()` with `getArrayCopy()`
|
||||
- Updates where almost all attributes of the document change at once (sparse update provides little benefit compared to passing the full document)
|
||||
- Complex nested relationship logic where full document state is required
|
||||
|
||||
## 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.
|
||||
|
||||
## Adding new endpoints
|
||||
|
||||
When adding new endpoints, make sure to use modules and follow its patterns. Find instruction in [Modules AGENTS.md](src/Appwrite/Platform/AGENTS.md) file.
|
||||
|
||||
## 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
|
||||
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
|
||||
|
||||
@@ -64,7 +64,7 @@ return [
|
||||
[
|
||||
'$id' => ID::custom('database'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 128,
|
||||
'size' => 2000,
|
||||
'required' => false,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
|
||||
+35
-24
@@ -250,26 +250,16 @@ return [
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'markdown',
|
||||
'name' => 'Markdown',
|
||||
'version' => '0.3.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-md.git',
|
||||
'package' => 'https://www.npmjs.com/package/@appwrite.io/docs',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'prism' => 'markdown',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-md'),
|
||||
'gitUrl' => 'git@github.com:appwrite/sdk-for-md.git',
|
||||
'gitRepoName' => 'sdk-for-md',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/md/CHANGELOG.md'),
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
APP_SDK_PLATFORM_STATIC => [
|
||||
'key' => APP_SDK_PLATFORM_STATIC,
|
||||
'name' => 'Static',
|
||||
'description' => 'SDK artifacts for Appwrite integrations that do not require a generated platform API specification.',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'sdks' => [
|
||||
[
|
||||
'key' => 'agent-skills',
|
||||
'name' => 'AgentSkills',
|
||||
@@ -279,9 +269,10 @@ return [
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'prism' => 'agent-skills',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-agent-skills'),
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-agent-skills'),
|
||||
'gitUrl' => 'git@github.com:appwrite/agent-skills.git',
|
||||
'gitRepoName' => 'agent-skills',
|
||||
'gitUserName' => 'appwrite',
|
||||
@@ -298,9 +289,10 @@ return [
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'prism' => 'cursor-plugin',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-cursor-plugin'),
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-cursor-plugin'),
|
||||
'gitUrl' => 'git@github.com:appwrite/cursor-plugin.git',
|
||||
'gitRepoName' => 'cursor-plugin',
|
||||
'gitUserName' => 'appwrite',
|
||||
@@ -494,6 +486,25 @@ return [
|
||||
'gitBranch' => 'dev',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/swift/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'rust',
|
||||
'name' => 'Rust',
|
||||
'version' => '0.1.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-rust',
|
||||
'package' => 'https://crates.io/crates/appwrite',
|
||||
'enabled' => true,
|
||||
'beta' => true,
|
||||
'dev' => true,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_SERVER,
|
||||
'prism' => 'rust',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/server-rust'),
|
||||
'gitUrl' => 'git@github.com:appwrite/sdk-for-rust.git',
|
||||
'gitRepoName' => 'sdk-for-rust',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/rust/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'graphql',
|
||||
'name' => 'GraphQL',
|
||||
|
||||
@@ -28,12 +28,18 @@ use Utopia\Validator\Text;
|
||||
Http::init()
|
||||
->groups(['graphql'])
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('authorization')
|
||||
->action(function (Document $project, Authorization $authorization) {
|
||||
->action(function (Document $project, User $user, Request $request, Response $response, Authorization $authorization) {
|
||||
$response->setUser($user);
|
||||
$request->setUser($user);
|
||||
|
||||
if (
|
||||
array_key_exists('graphql', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['graphql']
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
|
||||
@@ -1270,7 +1270,16 @@ Http::error()
|
||||
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
|
||||
*/
|
||||
if (!$publish && $project->getId() !== 'console') {
|
||||
if (!DBUser::isPrivileged($authorization->getRoles())) {
|
||||
$errorUser = new DBUser();
|
||||
try {
|
||||
$resolvedUser = $utopia->getResource('user');
|
||||
if ($resolvedUser instanceof DBUser) {
|
||||
$errorUser = $resolvedUser;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// User resource may not be available in error context
|
||||
}
|
||||
if (!$errorUser->isPrivileged($authorization->getRoles())) {
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
|
||||
@@ -96,7 +96,7 @@ Http::init()
|
||||
->inject('team')
|
||||
->inject('apiKey')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
/**
|
||||
@@ -419,7 +419,7 @@ Http::init()
|
||||
if (
|
||||
array_key_exists($namespace, $project->getAttribute('services', []))
|
||||
&& ! $project->getAttribute('services', [])[$namespace]
|
||||
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
@@ -483,7 +483,10 @@ Http::init()
|
||||
->inject('telemetry')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
|
||||
|
||||
$response->setUser($user);
|
||||
$request->setUser($user);
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
$path = $route->getMatchedPath();
|
||||
@@ -496,7 +499,7 @@ Http::init()
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
&& ! $project->getAttribute('apis', [])['rest']
|
||||
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -528,8 +531,8 @@ Http::init()
|
||||
$closestLimit = null;
|
||||
|
||||
$roles = $authorization->getRoles();
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
$isPrivilegedUser = $user->isPrivileged($roles);
|
||||
$isAppUser = $user->isApp($roles);
|
||||
|
||||
foreach ($timeLimitArray as $timeLimit) {
|
||||
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
|
||||
@@ -611,7 +614,7 @@ Http::init()
|
||||
if ($useCache) {
|
||||
$route = $utopia->match($request);
|
||||
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! User::isPrivileged($authorization->getRoles());
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
@@ -630,7 +633,7 @@ Http::init()
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -663,7 +666,7 @@ Http::init()
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
// Do not update transformedAt if it's a console user
|
||||
if (! User::isPrivileged($authorization->getRoles())) {
|
||||
if (! $user->isPrivileged($authorization->getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
|
||||
$file->setAttribute('transformedAt', DateTime::now());
|
||||
@@ -697,7 +700,7 @@ Http::init()
|
||||
->groups(['session'])
|
||||
->inject('user')
|
||||
->inject('request')
|
||||
->action(function (Document $user, Request $request) {
|
||||
->action(function (User $user, Request $request) {
|
||||
if (\str_contains($request->getURI(), 'oauth2')) {
|
||||
return;
|
||||
}
|
||||
@@ -984,7 +987,7 @@ Http::shutdown()
|
||||
}
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
if (! User::isPrivileged($authorization->getRoles())) {
|
||||
if (! $user->isPrivileged($authorization->getRoles())) {
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
|
||||
@@ -36,8 +36,9 @@ Http::init()
|
||||
->inject('request')
|
||||
->inject('project')
|
||||
->inject('geodb')
|
||||
->inject('user')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) {
|
||||
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
|
||||
if (!empty($denylist && $project->getId() === 'console')) {
|
||||
$countries = explode(',', $denylist);
|
||||
@@ -50,8 +51,8 @@ Http::init()
|
||||
|
||||
$route = $utopia->match($request);
|
||||
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
|
||||
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
|
||||
return;
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
|
||||
$lines = explode("\n", $data, 3);
|
||||
$request = $lines[0];
|
||||
if (count($lines) > 1) {
|
||||
$domain = trim(explode('Host: ', $lines[1])[1]);
|
||||
$domain = trim(explode('Host: ', $lines[1])[1] ?? '');
|
||||
}
|
||||
|
||||
// Sync executions are considered risky
|
||||
|
||||
@@ -97,6 +97,7 @@ const APP_COMPUTE_DEPLOYMENT_MAX_RETENTION = 100 * 365; // 100 years
|
||||
const APP_SDK_PLATFORM_SERVER = 'server';
|
||||
const APP_SDK_PLATFORM_CLIENT = 'client';
|
||||
const APP_SDK_PLATFORM_CONSOLE = 'console';
|
||||
const APP_SDK_PLATFORM_STATIC = 'static';
|
||||
const APP_VCS_GITHUB_USERNAME = 'Appwrite';
|
||||
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
|
||||
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
|
||||
|
||||
@@ -432,8 +432,10 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
$jwtUserId = $payload['userId'] ?? '';
|
||||
if (! empty($jwtUserId)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $jwtUserId);
|
||||
} else {
|
||||
/** @var User $user */
|
||||
$user = $dbForProject->getDocument('users', $jwtUserId);
|
||||
}
|
||||
}
|
||||
@@ -453,6 +455,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
|
||||
/** @var User $accountKeyUser */
|
||||
$accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
|
||||
if (! $accountKeyUser->isEmpty()) {
|
||||
$key = $accountKeyUser->find(
|
||||
|
||||
+6
-6
@@ -518,7 +518,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
|
||||
$database = getProjectDB($project);
|
||||
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $user */
|
||||
/** @var User $user */
|
||||
$user = $database->getDocument('users', $userId);
|
||||
|
||||
$roles = $user->getRoles($database->getAuthorization());
|
||||
@@ -642,10 +642,14 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
if (
|
||||
array_key_exists('realtime', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['realtime']
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -656,10 +660,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Project is not accessible in this region. Please make sure you are using the correct endpoint');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*
|
||||
|
||||
@@ -13,7 +13,7 @@ $enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql'];
|
||||
$isLocalInstall = $isLocalInstall ?? false;
|
||||
|
||||
|
||||
$cardStep = min(4, $step);
|
||||
$cardStep = ($step === 5) ? 4 : $step;
|
||||
$stepFile = __DIR__ . "/installer/templates/steps/step-{$cardStep}.phtml";
|
||||
if (!is_file($stepFile)) {
|
||||
$stepFile = __DIR__ . "/installer/templates/steps/step-1.phtml";
|
||||
|
||||
@@ -478,6 +478,10 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.installer-page[data-upgrade='true'] .installer-step {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.action-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1808,3 +1812,92 @@ body {
|
||||
gap: var(--gap-s);
|
||||
}
|
||||
}
|
||||
|
||||
.migration-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--gap-l);
|
||||
padding: var(--space-6);
|
||||
background: var(--bgcolor-neutral-default);
|
||||
border-radius: var(--border-radius-m);
|
||||
outline: var(--border-width-s) solid var(--border-neutral);
|
||||
outline-offset: calc(var(--border-width-s) * -1);
|
||||
cursor: pointer;
|
||||
transition: outline-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.migration-option:hover {
|
||||
outline-color: var(--border-neutral-stronger);
|
||||
}
|
||||
|
||||
.migration-option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.migration-switch {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.migration-switch-track {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background: var(--bgcolor-neutral-invert-weaker);
|
||||
transition: background 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.migration-switch-thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--bgcolor-neutral-primary);
|
||||
transition: transform 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
#run-migration:checked ~ .migration-switch-track {
|
||||
background: var(--bgcolor-neutral-invert-weak);
|
||||
}
|
||||
|
||||
#run-migration:checked ~ .migration-switch-track .migration-switch-thumb {
|
||||
transform: translateX(12px);
|
||||
}
|
||||
|
||||
#run-migration:focus-visible ~ .migration-switch-track {
|
||||
box-shadow: 0 0 0 var(--border-width-l) var(--border-focus);
|
||||
}
|
||||
|
||||
.migration-hint {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--gap-s);
|
||||
padding: 0 var(--space-2);
|
||||
}
|
||||
|
||||
.migration-hint-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--fgcolor-neutral-tertiary);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.migration-hint-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.migration-code {
|
||||
padding: 1px 4px;
|
||||
border-radius: var(--border-radius-xs, 4px);
|
||||
background: var(--bgcolor-neutral-secondary);
|
||||
font-family: monospace;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
const { validateInstallRequest } = window.InstallerStepsProgress || {};
|
||||
|
||||
const isUpgrade = document.body?.dataset.upgrade === 'true';
|
||||
const stepFlow = isUpgrade ? [1, 4, 5] : [1, 2, 3, 4, 5];
|
||||
const stepFlow = isUpgrade ? [1, 6, 4, 5] : [1, 2, 3, 4, 5];
|
||||
const cardSteps = stepFlow.filter((step) => step !== 5);
|
||||
|
||||
const normalizeStep = (step) => {
|
||||
@@ -53,7 +53,7 @@
|
||||
let pendingStep = null;
|
||||
let pendingPushState = false;
|
||||
|
||||
const clampStep = (step) => Math.max(1, Math.min(5, step));
|
||||
const clampStep = (step) => Math.max(1, Math.min(6, step));
|
||||
const isInstallLocked = () => Boolean(window.InstallerSteps?.isInstallLocked?.());
|
||||
|
||||
const scrollToFirstError = (panel) => {
|
||||
@@ -399,11 +399,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if (action === 'next' && String(target) === '5' && typeof validateInstallRequest === 'function') {
|
||||
const isValid = await validateInstallRequest();
|
||||
if (!isValid) {
|
||||
return;
|
||||
if (action === 'next' && String(target) === '5') {
|
||||
if (typeof validateInstallRequest === 'function') {
|
||||
const isValid = await validateInstallRequest();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Clear stale install data from previous runs so initStep5
|
||||
// starts a fresh install instead of trying to resume.
|
||||
const { clearInstallLock, clearInstallId } = window.InstallerStepsState || {};
|
||||
clearInstallLock?.();
|
||||
clearInstallId?.();
|
||||
}
|
||||
if (isInstallLocked() && Number(target) !== 5) {
|
||||
requestStep(5, true);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
ENV_VARS: 'env-vars',
|
||||
DOCKER_CONTAINERS: 'docker-containers',
|
||||
ACCOUNT_SETUP: 'account-setup',
|
||||
MIGRATION: 'migration',
|
||||
SSL_CERTIFICATE: 'ssl-certificate',
|
||||
REDIRECT: 'redirect'
|
||||
});
|
||||
@@ -52,6 +53,11 @@
|
||||
id: STEP_IDS.DOCKER_CONTAINERS,
|
||||
inProgress: 'Restarting Docker containers...',
|
||||
done: 'Docker containers restarted'
|
||||
},
|
||||
{
|
||||
id: STEP_IDS.MIGRATION,
|
||||
inProgress: 'Running database migration...',
|
||||
done: 'Database migration completed'
|
||||
}
|
||||
] : [
|
||||
{
|
||||
@@ -95,7 +101,7 @@
|
||||
const clampStep = (step) => {
|
||||
const numeric = Number(step);
|
||||
if (Number.isNaN(numeric)) return 1;
|
||||
return Math.max(1, Math.min(5, numeric));
|
||||
return Math.max(1, Math.min(6, numeric));
|
||||
};
|
||||
|
||||
window.InstallerStepsContext = Object.freeze({
|
||||
|
||||
@@ -373,7 +373,8 @@
|
||||
opensslKey: (formState?.opensslKey || '').trim(),
|
||||
assistantOpenAIKey: normalizedAssistantKey,
|
||||
accountEmail: normalizedAccountEmail,
|
||||
accountPassword: normalizedAccountPassword
|
||||
accountPassword: normalizedAccountPassword,
|
||||
migrate: formState?.migrate ?? false
|
||||
};
|
||||
};
|
||||
|
||||
@@ -721,7 +722,8 @@
|
||||
});
|
||||
startSyncedSpinnerRotation(list);
|
||||
|
||||
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
|
||||
const completeId = activeInstall?.installId || getStoredInstallId?.();
|
||||
notifyInstallComplete(completeId, sessionDetails).finally(() => {
|
||||
setTimeout(() => redirectToApp(protocol), TIMINGS?.redirectDelay ?? 0);
|
||||
});
|
||||
};
|
||||
@@ -911,21 +913,28 @@
|
||||
};
|
||||
|
||||
const isSnapshotTerminal = (snapshot) => {
|
||||
if (!snapshot?.steps) return true;
|
||||
if (!snapshot?.steps) return 'empty';
|
||||
const stepEntries = Object.values(snapshot.steps);
|
||||
if (stepEntries.length === 0) return true;
|
||||
if (stepEntries.length === 0) return 'empty';
|
||||
const hasError = stepEntries.some((s) => s.status === STATUS.ERROR);
|
||||
if (hasError) return true;
|
||||
if (hasError) return 'error';
|
||||
const allCompleted = INSTALLATION_STEPS.every((step) => {
|
||||
const detail = snapshot.steps[step.id];
|
||||
return detail && detail.status === STATUS.COMPLETED;
|
||||
});
|
||||
return allCompleted;
|
||||
if (allCompleted) return 'completed';
|
||||
return false;
|
||||
};
|
||||
|
||||
const resumeInstall = async (installId) => {
|
||||
const snapshot = await fetchInstallStatus(installId);
|
||||
if (!snapshot || isSnapshotTerminal(snapshot)) return false;
|
||||
const terminal = isSnapshotTerminal(snapshot);
|
||||
if (!snapshot || terminal) {
|
||||
if (terminal === 'completed') {
|
||||
return 'completed';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
activeInstall = {
|
||||
installId,
|
||||
controller: new AbortController(),
|
||||
@@ -1069,14 +1078,33 @@
|
||||
startInstallStream(newInstallId);
|
||||
};
|
||||
|
||||
const recoverToLastStep = () => {
|
||||
clearInstallId?.();
|
||||
clearInstallLock?.();
|
||||
const url = new URL(window.location.href);
|
||||
const lastStep = url.searchParams.get('step');
|
||||
// Stay on the current URL so the user keeps their place;
|
||||
// only navigate away if we're already on step 5 (the
|
||||
// progress screen) since there's nothing to show.
|
||||
if (!lastStep || String(lastStep) === '5') {
|
||||
window.location.href = '/?step=1';
|
||||
}
|
||||
};
|
||||
|
||||
const lock = getInstallLock?.();
|
||||
const existingInstallId = lock?.installId || getStoredInstallId?.();
|
||||
if (existingInstallId) {
|
||||
resumeInstall(existingInstallId).then((resumed) => {
|
||||
if (!resumed) {
|
||||
clearInstallId?.();
|
||||
resumeInstall(existingInstallId).then((result) => {
|
||||
if (result === 'completed') {
|
||||
// Install already finished — redirect to console
|
||||
// instead of bouncing back to step 1.
|
||||
stopSyncedSpinnerRotation();
|
||||
setUnloadGuard(false);
|
||||
clearInstallLock?.();
|
||||
window.location.href = '/?step=1';
|
||||
clearInstallId?.();
|
||||
startSslCheck(null);
|
||||
} else if (!result) {
|
||||
recoverToLastStep();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -329,6 +329,30 @@
|
||||
}
|
||||
};
|
||||
|
||||
const initStep6 = (root) => {
|
||||
if (!root) return;
|
||||
syncInstallLockFlag?.();
|
||||
applyLockPayload?.();
|
||||
applyBodyDefaults?.();
|
||||
|
||||
const checkbox = root.querySelector('#run-migration');
|
||||
if (checkbox) {
|
||||
if (formState.migrate !== undefined) {
|
||||
checkbox.checked = formState.migrate;
|
||||
} else {
|
||||
formState.migrate = checkbox.checked;
|
||||
}
|
||||
checkbox.addEventListener('change', () => {
|
||||
formState.migrate = checkbox.checked;
|
||||
dispatchStateChange?.('migrate');
|
||||
});
|
||||
}
|
||||
|
||||
if (isInstallLocked?.()) {
|
||||
disableControls?.(root);
|
||||
}
|
||||
};
|
||||
|
||||
const initStep = (step, container) => {
|
||||
if (!container) return;
|
||||
const root = container.querySelector('.step-layout') || container;
|
||||
@@ -346,6 +370,7 @@
|
||||
if (normalized === 3) initStep3(root);
|
||||
if (normalized === 4) initStep4(root);
|
||||
if (normalized === 5) Progress.initStep5?.(root);
|
||||
if (normalized === 6) initStep6(root);
|
||||
};
|
||||
|
||||
window.InstallerSteps = {
|
||||
|
||||
@@ -62,12 +62,14 @@ $badgeClass = $defaultSecretKey !== '' ? 'badge-success' : 'badge-warning';
|
||||
<span class="badge badge-neutral typography-text-xs-400" data-review-assistant-badge>Disabled</span>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Appwrite Assistant</div>
|
||||
</div>
|
||||
<?php if (!$isUpgrade) { ?>
|
||||
<div class="review-row">
|
||||
<span class="badge <?php echo $badgeClass; ?> typography-text-xs-400" data-review-badge>
|
||||
<?php echo htmlspecialchars((string) $badgeLabel, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</span>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Secret API key</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
<div class="install-panel">
|
||||
<div class="install-header">
|
||||
<div class="typography-text-m-400 text-neutral-primary">
|
||||
<?php echo $isUpgrade ? 'Updating your app…' : 'Installing your app…'; ?>
|
||||
<?php echo $isUpgrade ? 'Updating Appwrite…' : 'Installing Appwrite…'; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-list" data-install-list></div>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
$isUpgrade = $isUpgrade ?? false;
|
||||
?>
|
||||
<div class="step-layout" data-step="6">
|
||||
<div class="stack-xl">
|
||||
<div class="stack-xxxs">
|
||||
<h1 class="typography-title-s text-neutral-primary">Database migration</h1>
|
||||
<p class="typography-text-m-400 text-neutral-secondary">
|
||||
Run database migration after the update to apply schema changes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="stack-xl">
|
||||
<label class="migration-option" for="run-migration">
|
||||
<span class="migration-option-content">
|
||||
<span class="typography-text-m-500 text-neutral-primary">Run migration automatically</span>
|
||||
<span class="typography-text-xs-400 text-neutral-tertiary">Recommended when upgrading to a new version</span>
|
||||
</span>
|
||||
<span class="migration-switch">
|
||||
<input type="checkbox" id="run-migration" name="migrate" class="sr-only" checked>
|
||||
<span class="migration-switch-track" aria-hidden="true">
|
||||
<span class="migration-switch-thumb"></span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="migration-hint">
|
||||
<span class="migration-hint-icon">
|
||||
<?php include __DIR__ . '/../../icons/info.svg'; ?>
|
||||
</span>
|
||||
<span class="typography-text-xs-400 text-neutral-tertiary">
|
||||
To run manually later: <code class="migration-code">docker compose exec appwrite migrate</code>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Generated
+45
-45
@@ -1226,16 +1226,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/api",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/api.git",
|
||||
"reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad"
|
||||
"reference": "6f8d237ce2c304ca85f31970f788e7f074d147be"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/df5197c6fd0ddd8e9883b87de042d9341300e2ad",
|
||||
"reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/6f8d237ce2c304ca85f31970f788e7f074d147be",
|
||||
"reference": "6f8d237ce2c304ca85f31970f788e7f074d147be",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1292,20 +1292,20 @@
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2026-01-21T04:14:03+00:00"
|
||||
"time": "2026-02-25T13:24:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/context",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/context.git",
|
||||
"reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf"
|
||||
"reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf",
|
||||
"reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/context/zipball/3c414b246e0dabb7d6145404e6a5e4536ca18d07",
|
||||
"reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1347,11 +1347,11 @@
|
||||
],
|
||||
"support": {
|
||||
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
|
||||
"docs": "https://opentelemetry.io/docs/php",
|
||||
"docs": "https://opentelemetry.io/docs/languages/php",
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-09-19T00:05:49+00:00"
|
||||
"time": "2025-10-19T06:44:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/exporter-otlp",
|
||||
@@ -1419,16 +1419,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/gen-otlp-protobuf",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git",
|
||||
"reference": "673af5b06545b513466081884b47ef15a536edde"
|
||||
"reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde",
|
||||
"reference": "673af5b06545b513466081884b47ef15a536edde",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/a229cf161d42001d64c8f21e8f678581fe1c66b9",
|
||||
"reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1474,30 +1474,30 @@
|
||||
],
|
||||
"support": {
|
||||
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
|
||||
"docs": "https://opentelemetry.io/docs/php",
|
||||
"docs": "https://opentelemetry.io/docs/languages/php",
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-09-17T23:10:12+00:00"
|
||||
"time": "2025-10-19T06:44:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sdk",
|
||||
"version": "1.13.0",
|
||||
"version": "1.14.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/sdk.git",
|
||||
"reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1"
|
||||
"reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/c76f91203bf7ef98ab3f4e0a82ca21699af185e1",
|
||||
"reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/6e3d0ce93e76555dd5e2f1d19443ff45b990e410",
|
||||
"reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"nyholm/psr7-server": "^1.1",
|
||||
"open-telemetry/api": "^1.7",
|
||||
"open-telemetry/api": "^1.8",
|
||||
"open-telemetry/context": "^1.4",
|
||||
"open-telemetry/sem-conv": "^1.0",
|
||||
"php": "^8.1",
|
||||
@@ -1575,7 +1575,7 @@
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2026-01-28T11:38:11+00:00"
|
||||
"time": "2026-03-21T11:50:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sem-conv",
|
||||
@@ -3403,16 +3403,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/agents",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/agents.git",
|
||||
"reference": "052227953678a30ecc4b5467401fcb0b2386471e"
|
||||
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e",
|
||||
"reference": "052227953678a30ecc4b5467401fcb0b2386471e",
|
||||
"url": "https://api.github.com/repos/utopia-php/agents/zipball/06064fd9fb19b77ae45a12ec7bcbc17670912c30",
|
||||
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3450,9 +3450,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/agents/issues",
|
||||
"source": "https://github.com/utopia-php/agents/tree/1.2.1"
|
||||
"source": "https://github.com/utopia-php/agents/tree/1.3.0"
|
||||
},
|
||||
"time": "2026-02-24T06:03:55+00:00"
|
||||
"time": "2026-03-26T03:51:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/analytics",
|
||||
@@ -4002,16 +4002,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dns",
|
||||
"version": "1.6.5",
|
||||
"version": "1.6.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/dns.git",
|
||||
"reference": "574327f0f5fabefa7048030c5634cde33ad10640"
|
||||
"reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/574327f0f5fabefa7048030c5634cde33ad10640",
|
||||
"reference": "574327f0f5fabefa7048030c5634cde33ad10640",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/917901ecfe5f09a540e4f689b6cbb80b9f55035d",
|
||||
"reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4053,9 +4053,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/dns/issues",
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.6.5"
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.6.6"
|
||||
},
|
||||
"time": "2026-02-19T16:06:46+00:00"
|
||||
"time": "2026-03-27T11:13:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
@@ -5439,16 +5439,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.12.1",
|
||||
"version": "1.14.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "a724aa8db52f83ea35854a004837fa5ce990b736"
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/a724aa8db52f83ea35854a004837fa5ce990b736",
|
||||
"reference": "a724aa8db52f83ea35854a004837fa5ce990b736",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5484,9 +5484,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.12.1"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.14.0"
|
||||
},
|
||||
"time": "2026-03-24T05:18:43+00:00"
|
||||
"time": "2026-03-26T12:50:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
@@ -6195,11 +6195,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.42",
|
||||
"version": "2.1.44",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0",
|
||||
"reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6244,7 +6244,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-17T14:58:32+00:00"
|
||||
"time": "2026-03-25T17:34:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
## 0.3.0
|
||||
|
||||
* Add `bytesMax` and `bytesUsed` properties to Collection and Table documentation
|
||||
* Add `queries` parameter to `listKeys` and `keyId` parameter to `createKey` documentation
|
||||
* Add `dart-3.10` and `flutter-3.38` runtimes
|
||||
* Fix Teams membership docs to use `string[]` instead of `Roles[]`
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* Document array-based enum parameters in Markdown examples (e.g., `permissions: BrowserPermission[]`).
|
||||
* Breaking change: `Output` enum has been removed; use `ImageFormat` instead.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
* Initial release
|
||||
@@ -0,0 +1,5 @@
|
||||
# Change Log
|
||||
|
||||
## 0.1.0
|
||||
|
||||
* Initial release
|
||||
@@ -0,0 +1,68 @@
|
||||
## Getting Started
|
||||
|
||||
### Init your SDK
|
||||
Initialize your SDK with your Appwrite server API endpoint and project ID which can be found on your project settings page and your new API secret Key from project's API keys section.
|
||||
|
||||
```rust
|
||||
use appwrite::client::Client;
|
||||
|
||||
let client = Client::new()
|
||||
.set_endpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
|
||||
.set_project("5df5acd0d48c2") // Your project ID
|
||||
.set_key("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key
|
||||
.set_self_signed(true); // Use only on dev mode with a self-signed SSL cert
|
||||
```
|
||||
|
||||
### Make Your First Request
|
||||
Once your SDK object is set, create any of the Appwrite service objects and choose any request to send. Full documentation for any service method you would like to use can be found in your SDK documentation or in the [API References](https://appwrite.io/docs) section.
|
||||
|
||||
```rust
|
||||
use appwrite::client::Client;
|
||||
use appwrite::services::users::Users;
|
||||
use appwrite::id::ID;
|
||||
|
||||
let client = Client::new()
|
||||
.set_endpoint("https://[HOSTNAME_OR_IP]/v1")
|
||||
.set_project("5df5acd0d48c2")
|
||||
.set_key("919c2d18fb5d4...a2ae413da83346ad2")
|
||||
.set_self_signed(true);
|
||||
|
||||
let users = Users::new(&client);
|
||||
|
||||
let user = users.create(
|
||||
ID::unique(),
|
||||
Some("email@example.com"),
|
||||
Some("+123456789"),
|
||||
Some("password"),
|
||||
Some("Walter O'Brien"),
|
||||
).await?;
|
||||
|
||||
println!("{}", user.name);
|
||||
println!("{}", user.email);
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
The Appwrite Rust SDK returns `Result` types. You can handle errors using standard Rust error handling patterns. Below is an example.
|
||||
|
||||
```rust
|
||||
use appwrite::error::AppwriteError;
|
||||
|
||||
match users.create(
|
||||
ID::unique(),
|
||||
Some("email@example.com"),
|
||||
Some("+123456789"),
|
||||
Some("password"),
|
||||
Some("Walter O'Brien"),
|
||||
).await {
|
||||
Ok(user) => println!("{}", user.name),
|
||||
Err(AppwriteError { message, code, .. }) => {
|
||||
eprintln!("Error {}: {}", code, message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Learn more
|
||||
You can use the following resources to learn more and get help
|
||||
- 🚀 [Getting Started Tutorial](https://appwrite.io/docs/getting-started-for-server)
|
||||
- 📜 [Appwrite Docs](https://appwrite.io/docs)
|
||||
- 💬 [Discord Community](https://appwrite.io/discord)
|
||||
@@ -114,12 +114,6 @@ parameters:
|
||||
count: 1
|
||||
path: app/controllers/mock.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Utopia\\Database\\Document\:\:getRoles\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: app/controllers/shared/api.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$register might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
@@ -594,12 +588,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$currentDocumentId on left side of \?\? always exists and is always null\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^Offset ''deviceBrand'' does not exist on int\.$#'
|
||||
identifier: offsetAccess.notFound
|
||||
|
||||
@@ -43,6 +43,7 @@ class Install extends Action
|
||||
->param('database', '', new WhiteList(['mongodb', 'mariadb', 'postgresql']), 'Database adapter', true)
|
||||
->param('installId', '', new Text(64, 0), 'Installation ID', true)
|
||||
->param('retryStep', null, new Nullable(new WhiteList([Server::STEP_DOCKER_COMPOSE, Server::STEP_ENV_VARS, Server::STEP_DOCKER_CONTAINERS], true)), 'Retry from step', true)
|
||||
->param('migrate', false, new \Utopia\Validator\Boolean(true), 'Run database migration after upgrade', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('swooleResponse')
|
||||
@@ -64,6 +65,7 @@ class Install extends Action
|
||||
string $database,
|
||||
string $installId,
|
||||
?string $retryStep,
|
||||
bool $migrate,
|
||||
Request $request,
|
||||
Response $response,
|
||||
SwooleResponse $swooleResponse,
|
||||
@@ -321,6 +323,28 @@ class Install extends Action
|
||||
}
|
||||
};
|
||||
|
||||
$responseSent = false;
|
||||
$onComplete = function () use ($wantsStream, $swooleResponse, $response, $installId, $state, &$responseSent) {
|
||||
if ($responseSent) {
|
||||
return;
|
||||
}
|
||||
$responseSent = true;
|
||||
$state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, 'done', ['installId' => $installId, 'success' => true]);
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->write(": keepalive\n\n");
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->end();
|
||||
} else {
|
||||
$response->json([
|
||||
'success' => true,
|
||||
'installId' => $installId,
|
||||
'message' => 'Installation completed successfully',
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
$installer->performInstallation(
|
||||
$httpPort ?: $config->getDefaultHttpPort(),
|
||||
$httpsPort ?: $config->getDefaultHttpsPort(),
|
||||
@@ -331,23 +355,12 @@ class Install extends Action
|
||||
$progress,
|
||||
$retryStep,
|
||||
$config->isUpgrade(),
|
||||
$account
|
||||
$account,
|
||||
$onComplete,
|
||||
$migrate,
|
||||
);
|
||||
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, 'done', ['installId' => $installId, 'success' => true]);
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->write(": keepalive\n\n");
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->end();
|
||||
} else {
|
||||
$response->json([
|
||||
'success' => true,
|
||||
'installId' => $installId,
|
||||
'message' => 'Installation completed successfully',
|
||||
]);
|
||||
}
|
||||
$state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
|
||||
$onComplete();
|
||||
} catch (\Throwable $e) {
|
||||
$this->handleInstallationError($e, $installId, $wantsStream, $response, $swooleResponse, $state);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class View extends Action
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/')
|
||||
->desc('Serve installer UI')
|
||||
->param('step', 1, new Integer(true), 'Step number (1-5)', true)
|
||||
->param('step', 1, new Integer(true), 'Step number (1-6)', true)
|
||||
->param('partial', null, new Nullable(new Text(1, 0)), 'Render partial step only', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
@@ -52,10 +52,13 @@ class View extends Action
|
||||
$defaultEmailCertificates = 'walterobrien@example.com';
|
||||
}
|
||||
|
||||
$step = max(1, min(5, $step));
|
||||
$step = max(1, min(6, $step));
|
||||
if ($isUpgrade && ($step === 2 || $step === 3)) {
|
||||
$step = 4;
|
||||
}
|
||||
if (!$isUpgrade && $step === 6) {
|
||||
$step = 4;
|
||||
}
|
||||
|
||||
$partialFile = $paths['views'] . "/installer/templates/steps/step-{$step}.phtml";
|
||||
if (!is_file($partialFile)) {
|
||||
|
||||
@@ -6,6 +6,7 @@ use Appwrite\Platform\Installer\Http\Installer\Error;
|
||||
use Appwrite\Platform\Installer\Runtime\Config;
|
||||
use Appwrite\Platform\Installer\Runtime\State;
|
||||
use Swoole\Http\Server as SwooleServer;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\Http\Adapter\Swoole\Request;
|
||||
use Utopia\Http\Adapter\Swoole\Response;
|
||||
use Utopia\Http\Adapter\Swoole\Server as SwooleAdapter;
|
||||
@@ -28,6 +29,7 @@ class Server
|
||||
public const string STEP_DOCKER_COMPOSE = 'docker-compose';
|
||||
public const string STEP_DOCKER_CONTAINERS = 'docker-containers';
|
||||
public const string STEP_ACCOUNT_SETUP = 'account-setup';
|
||||
public const string STEP_MIGRATION = 'migration';
|
||||
public const string STEP_SSL_CERTIFICATE = 'ssl-certificate';
|
||||
|
||||
public const string STATUS_IN_PROGRESS = 'in-progress';
|
||||
@@ -129,6 +131,8 @@ class Server
|
||||
|
||||
private function startSwooleServer(string $host, int $port, ?string $readyFile = null): void
|
||||
{
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
|
||||
$this->state->clearStaleLock();
|
||||
|
||||
// Preload static files into memory
|
||||
|
||||
+4
-3
@@ -87,13 +87,14 @@ class Decrement extends Action
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
|
||||
+4
-3
@@ -87,13 +87,14 @@ class Increment extends Action
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
|
||||
+3
-3
@@ -139,7 +139,7 @@ class Create extends Action
|
||||
->inject('eventProcessor')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Document $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
{
|
||||
$data = \is_string($data)
|
||||
? \json_decode($data, true)
|
||||
@@ -183,8 +183,8 @@ class Create extends Action
|
||||
$documents = [$data];
|
||||
}
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($isBulk && !$isAPIKey && !$isPrivilegedUser) {
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE);
|
||||
|
||||
+5
-3
@@ -85,6 +85,7 @@ class Delete extends Action
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -101,12 +102,13 @@ class Delete extends Action
|
||||
Context $usage,
|
||||
TransactionState $transactionState,
|
||||
array $plan,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
): void {
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
|
||||
+4
-3
@@ -72,13 +72,14 @@ class Get extends Action
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization, User $user): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
+4
-3
@@ -89,10 +89,11 @@ class Update extends Action
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization, User $user): void
|
||||
{
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
@@ -102,8 +103,8 @@ class Update extends Action
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
|
||||
+3
-3
@@ -96,7 +96,7 @@ class Upsert extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, User $user, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
{
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
@@ -108,8 +108,8 @@ class Upsert extends Action
|
||||
throw new Exception($this->getMissingPayloadException());
|
||||
}
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
+3
-3
@@ -83,10 +83,10 @@ class XList extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, User $user, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
+4
-3
@@ -65,17 +65,18 @@ class Create extends Action
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $transactionId, array $operations, UtopiaResponse $response, Database $dbForProject, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
public function action(string $transactionId, array $operations, UtopiaResponse $response, Database $dbForProject, TransactionState $transactionState, array $plan, Authorization $authorization, User $user): void
|
||||
{
|
||||
if (empty($operations)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty');
|
||||
}
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
// API keys and admins can read any transaction, regular users need permissions
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
|
||||
@@ -91,7 +91,7 @@ class Update extends Action
|
||||
* @param UtopiaResponse $response
|
||||
* @param Database $dbForProject
|
||||
* @param callable $getDatabasesDB
|
||||
* @param Document $user
|
||||
* @param User $user
|
||||
* @param TransactionState $transactionState
|
||||
* @param Delete $queueForDeletes
|
||||
* @param Event $queueForEvents
|
||||
@@ -109,7 +109,7 @@ class Update extends Action
|
||||
* @throws Structure
|
||||
* @throws \Utopia\Http\Exception
|
||||
*/
|
||||
public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
{
|
||||
if (!$commit && !$rollback) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true');
|
||||
@@ -118,8 +118,8 @@ class Update extends Action
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time');
|
||||
}
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$transaction = ($isAPIKey || $isPrivilegedUser)
|
||||
? $authorization->skip(fn () => $dbForProject->getDocument('transactions', $transactionId))
|
||||
|
||||
+1
@@ -68,6 +68,7 @@ class Decrement extends DecrementDocumentAttribute
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -68,6 +68,7 @@ class Increment extends IncrementDocumentAttribute
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ class Delete extends DocumentDelete
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ class Get extends DocumentGet
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -70,6 +70,7 @@ class Update extends DocumentUpdate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class Create extends IndexCreate
|
||||
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
|
||||
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).', false, ['dbForProject'])
|
||||
->param('key', null, fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject'])
|
||||
->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL]), 'Index type.')
|
||||
->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.')
|
||||
->param('attributes', null, fn (Database $dbForProject) => new ArrayList(new Key(true, $dbForProject->getAdapter()->getMaxUIDLength()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.', false, ['dbForProject'])
|
||||
->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true)
|
||||
->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true)
|
||||
|
||||
+1
@@ -55,6 +55,7 @@ class Create extends OperationsCreate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ class Decrement extends DecrementDocumentAttribute
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ class Increment extends IncrementDocumentAttribute
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ class Delete extends DocumentDelete
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ class Get extends DocumentGet
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ class Update extends DocumentUpdate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ class Create extends OperationsCreate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ class Delete extends DocumentDelete
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ class Get extends DocumentGet
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -70,6 +70,7 @@ class Update extends DocumentUpdate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -55,6 +55,7 @@ class Create extends OperationsCreate
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class Create extends Base
|
||||
Document $project,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Document $user,
|
||||
User $user,
|
||||
Event $queueForEvents,
|
||||
Context $usage,
|
||||
Func $queueForFunctions,
|
||||
@@ -171,8 +171,8 @@ class Create extends Base
|
||||
/* @var Document $function */
|
||||
$function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
|
||||
@@ -53,6 +53,7 @@ class Get extends Base
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -61,12 +62,13 @@ class Get extends Base
|
||||
string $executionId,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
|
||||
@@ -62,6 +62,7 @@ class XList extends Base
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -71,12 +72,13 @@ class XList extends Base
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
|
||||
@@ -103,7 +103,7 @@ class Create extends Action
|
||||
Request $request,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Document $user,
|
||||
User $user,
|
||||
Event $queueForEvents,
|
||||
string $mode,
|
||||
Device $deviceForFiles,
|
||||
@@ -112,8 +112,8 @@ class Create extends Action
|
||||
) {
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -66,6 +66,7 @@ class Delete extends Action
|
||||
->inject('deviceForFiles')
|
||||
->inject('queueForDeletes')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -77,12 +78,13 @@ class Delete extends Action
|
||||
Event $queueForEvents,
|
||||
Device $deviceForFiles,
|
||||
DeleteEvent $queueForDeletes,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -70,6 +70,7 @@ class Get extends Action
|
||||
->inject('resourceToken')
|
||||
->inject('deviceForFiles')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -84,12 +85,13 @@ class Get extends Action
|
||||
Document $resourceToken,
|
||||
Device $deviceForFiles,
|
||||
Authorization $authorization,
|
||||
User $user,
|
||||
) {
|
||||
/* @type Document $bucket */
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -51,6 +51,7 @@ class Get extends Action
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -59,12 +60,13 @@ class Get extends Action
|
||||
string $fileId,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -92,6 +92,7 @@ class Get extends Action
|
||||
->inject('deviceForLocal')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -117,7 +118,8 @@ class Get extends Action
|
||||
Device $deviceForFiles,
|
||||
Device $deviceForLocal,
|
||||
Document $project,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
|
||||
if (!\extension_loaded('imagick')) {
|
||||
@@ -127,8 +129,8 @@ class Get extends Action
|
||||
/* @type Document $bucket */
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -271,7 +273,7 @@ class Get extends Action
|
||||
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
|
||||
|
||||
//Do not update transformedAt if it's a console user
|
||||
if (!User::isPrivileged($authorization->getRoles())) {
|
||||
if (!$user->isPrivileged($authorization->getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
|
||||
$file->setAttribute('transformedAt', DateTime::now());
|
||||
|
||||
@@ -52,6 +52,7 @@ class Get extends Action
|
||||
->inject('mode')
|
||||
->inject('deviceForFiles')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -66,7 +67,8 @@ class Get extends Action
|
||||
Document $project,
|
||||
string $mode,
|
||||
Device $deviceForFiles,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
|
||||
|
||||
@@ -88,8 +90,8 @@ class Get extends Action
|
||||
$disposition = $decoded['disposition'] ?? 'inline';
|
||||
$dbForProject = $isInternal ? $dbForPlatform : $dbForProject;
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
@@ -64,6 +64,7 @@ class Update extends Action
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -75,12 +76,13 @@ class Update extends Action
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -108,7 +110,7 @@ class Update extends Action
|
||||
|
||||
// Users can only manage their own roles, API keys and Admin users can manage any
|
||||
$roles = $authorization->getRoles();
|
||||
if (!User::isApp($roles) && !User::isPrivileged($roles) && !\is_null($permissions)) {
|
||||
if (!$user->isApp($roles) && !$user->isPrivileged($roles) && !\is_null($permissions)) {
|
||||
foreach (Database::PERMISSIONS as $type) {
|
||||
foreach ($permissions as $permission) {
|
||||
$permission = Permission::parse($permission);
|
||||
|
||||
@@ -71,6 +71,7 @@ class Get extends Action
|
||||
->inject('resourceToken')
|
||||
->inject('deviceForFiles')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -84,13 +85,14 @@ class Get extends Action
|
||||
string $mode,
|
||||
Document $resourceToken,
|
||||
Device $deviceForFiles,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
/* @type Document $bucket */
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -63,6 +63,7 @@ class XList extends Action
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -74,12 +75,13 @@ class XList extends Action
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
string $mode,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
) {
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -98,10 +98,10 @@ class Create extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, Password $proofForPassword, Token $proofForToken)
|
||||
public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, Password $proofForPassword, Token $proofForToken)
|
||||
{
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if (empty($url)) {
|
||||
if (! $isAppUser && ! $isPrivilegedUser) {
|
||||
|
||||
@@ -52,10 +52,11 @@ class Get extends Action
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject, Authorization $authorization)
|
||||
public function action(string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject, Authorization $authorization, User $user)
|
||||
{
|
||||
$team = $dbForProject->getDocument('teams', $teamId);
|
||||
|
||||
@@ -76,25 +77,25 @@ class Get extends Action
|
||||
];
|
||||
|
||||
$roles = $authorization->getRoles();
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
$isPrivilegedUser = $user->isPrivileged($roles);
|
||||
$isAppUser = $user->isApp($roles);
|
||||
|
||||
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
|
||||
return $privacy || $isPrivilegedUser || $isAppUser;
|
||||
}, $membershipsPrivacy);
|
||||
|
||||
$user = !empty(array_filter($membershipsPrivacy))
|
||||
$memberUser = !empty(array_filter($membershipsPrivacy))
|
||||
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
|
||||
: new Document();
|
||||
|
||||
if ($membershipsPrivacy['mfa']) {
|
||||
$mfa = $user->getAttribute('mfa', false);
|
||||
$mfa = $memberUser->getAttribute('mfa', false);
|
||||
|
||||
if ($mfa) {
|
||||
$totp = TOTP::getAuthenticatorFromUser($user);
|
||||
$totp = TOTP::getAuthenticatorFromUser($memberUser);
|
||||
$totpEnabled = $totp && $totp->getAttribute('verified', false);
|
||||
$emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false);
|
||||
$phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);
|
||||
$emailEnabled = $memberUser->getAttribute('email', false) && $memberUser->getAttribute('emailVerification', false);
|
||||
$phoneEnabled = $memberUser->getAttribute('phone', false) && $memberUser->getAttribute('phoneVerification', false);
|
||||
|
||||
if (!$totpEnabled && !$emailEnabled && !$phoneEnabled) {
|
||||
$mfa = false;
|
||||
@@ -105,11 +106,11 @@ class Get extends Action
|
||||
}
|
||||
|
||||
if ($membershipsPrivacy['userName']) {
|
||||
$membership->setAttribute('userName', $user->getAttribute('name'));
|
||||
$membership->setAttribute('userName', $memberUser->getAttribute('name'));
|
||||
}
|
||||
|
||||
if ($membershipsPrivacy['userEmail']) {
|
||||
$membership->setAttribute('userEmail', $user->getAttribute('email'));
|
||||
$membership->setAttribute('userEmail', $memberUser->getAttribute('email'));
|
||||
}
|
||||
|
||||
$membership->setAttribute('teamName', $team->getAttribute('name'));
|
||||
|
||||
@@ -66,7 +66,7 @@ class Update extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Event $queueForEvents)
|
||||
public function action(string $teamId, string $membershipId, array $roles, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Authorization $authorization, Event $queueForEvents)
|
||||
{
|
||||
$team = $dbForProject->getDocument('teams', $teamId);
|
||||
if ($team->isEmpty()) {
|
||||
@@ -83,8 +83,8 @@ class Update extends Action
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
$isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner');
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
|
||||
@@ -61,10 +61,11 @@ class XList extends Action
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject, Authorization $authorization)
|
||||
public function action(string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject, Authorization $authorization, User $user)
|
||||
{
|
||||
$team = $dbForProject->getDocument('teams', $teamId);
|
||||
|
||||
@@ -129,26 +130,26 @@ class XList extends Action
|
||||
];
|
||||
|
||||
$roles = $authorization->getRoles();
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
$isPrivilegedUser = $user->isPrivileged($roles);
|
||||
$isAppUser = $user->isApp($roles);
|
||||
|
||||
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
|
||||
return $privacy || $isPrivilegedUser || $isAppUser;
|
||||
}, $membershipsPrivacy);
|
||||
|
||||
$memberships = array_map(function ($membership) use ($dbForProject, $team, $membershipsPrivacy) {
|
||||
$user = !empty(array_filter($membershipsPrivacy))
|
||||
$memberUser = !empty(array_filter($membershipsPrivacy))
|
||||
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
|
||||
: new Document();
|
||||
|
||||
if ($membershipsPrivacy['mfa']) {
|
||||
$mfa = $user->getAttribute('mfa', false);
|
||||
$mfa = $memberUser->getAttribute('mfa', false);
|
||||
|
||||
if ($mfa) {
|
||||
$totp = TOTP::getAuthenticatorFromUser($user);
|
||||
$totp = TOTP::getAuthenticatorFromUser($memberUser);
|
||||
$totpEnabled = $totp && $totp->getAttribute('verified', false);
|
||||
$emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false);
|
||||
$phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);
|
||||
$emailEnabled = $memberUser->getAttribute('email', false) && $memberUser->getAttribute('emailVerification', false);
|
||||
$phoneEnabled = $memberUser->getAttribute('phone', false) && $memberUser->getAttribute('phoneVerification', false);
|
||||
|
||||
if (!$totpEnabled && !$emailEnabled && !$phoneEnabled) {
|
||||
$mfa = false;
|
||||
@@ -159,11 +160,11 @@ class XList extends Action
|
||||
}
|
||||
|
||||
if ($membershipsPrivacy['userName']) {
|
||||
$membership->setAttribute('userName', $user->getAttribute('name'));
|
||||
$membership->setAttribute('userName', $memberUser->getAttribute('name'));
|
||||
}
|
||||
|
||||
if ($membershipsPrivacy['userEmail']) {
|
||||
$membership->setAttribute('userEmail', $user->getAttribute('email'));
|
||||
$membership->setAttribute('userEmail', $memberUser->getAttribute('email'));
|
||||
}
|
||||
|
||||
$membership->setAttribute('teamName', $team->getAttribute('name'));
|
||||
|
||||
@@ -68,10 +68,10 @@ class Create extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents)
|
||||
public function action(string $teamId, string $name, array $roles, Response $response, User $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents)
|
||||
{
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
|
||||
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ use Utopia\Platform\Action as UtopiaAction;
|
||||
|
||||
class Action extends UtopiaAction
|
||||
{
|
||||
protected function getFileAndBucket(Database $dbForProject, Authorization $authorization, string $bucketId, string $fileId): array
|
||||
protected function getFileAndBucket(Database $dbForProject, Authorization $authorization, User $user, string $bucketId, string $fileId): array
|
||||
{
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
|
||||
@@ -8,6 +8,7 @@ use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Database\Database;
|
||||
@@ -64,19 +65,20 @@ class Create extends Action
|
||||
->param('fileId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'File unique ID.', false, ['dbForProject'])
|
||||
->param('expire', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Token expiry date', true)
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $bucketId, string $fileId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void
|
||||
public function action(string $bucketId, string $fileId, ?string $expire, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void
|
||||
{
|
||||
/**
|
||||
* @var Document $bucket
|
||||
* @var Document $file
|
||||
*/
|
||||
['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $authorization, $bucketId, $fileId);
|
||||
['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $authorization, $user, $bucketId, $fileId);
|
||||
|
||||
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
|
||||
$bucketPermission = $authorization->isValid(new Input(Database::PERMISSION_UPDATE, $bucket->getUpdate()));
|
||||
|
||||
@@ -7,6 +7,7 @@ use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\FileTokens;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
@@ -57,14 +58,15 @@ class XList extends Action
|
||||
->param('queries', [], new FileTokens(), '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(', ', FileTokens::ALLOWED_ATTRIBUTES), 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('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $bucketId, string $fileId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Authorization $authorization)
|
||||
public function action(string $bucketId, string $fileId, array $queries, bool $includeTotal, Response $response, User $user, Database $dbForProject, Authorization $authorization)
|
||||
{
|
||||
['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $authorization, $bucketId, $fileId);
|
||||
['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $authorization, $user, $bucketId, $fileId);
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
$queries[] = Query::equal('resourceType', [TOKENS_RESOURCE_TYPE_FILES]);
|
||||
|
||||
+4
-5
@@ -82,12 +82,11 @@ class Create extends Action
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_DETECTION_RUNTIME,
|
||||
model: [
|
||||
Response::MODEL_DETECTION_RUNTIME,
|
||||
Response::MODEL_DETECTION_FRAMEWORK,
|
||||
],
|
||||
),
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_DETECTION_FRAMEWORK,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('installationId', '', new Text(256), 'Installation Id')
|
||||
|
||||
@@ -86,12 +86,11 @@ class XList extends Action
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST,
|
||||
model: [
|
||||
Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST,
|
||||
Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST,
|
||||
],
|
||||
),
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('installationId', '', new Text(256), 'Installation Id')
|
||||
|
||||
@@ -7,6 +7,7 @@ use Appwrite\Docker\Env;
|
||||
use Appwrite\Platform\Installer\Runtime\State;
|
||||
use Appwrite\Platform\Installer\Server as InstallerServer;
|
||||
use Appwrite\Utopia\View;
|
||||
use Swoole\Coroutine;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Config\Config;
|
||||
@@ -35,6 +36,7 @@ class Install extends Action
|
||||
private const string GROWTH_API_URL = 'https://growth.appwrite.io/v1';
|
||||
|
||||
protected bool $isUpgrade = false;
|
||||
protected bool $migrate = false;
|
||||
protected string $hostPath = '';
|
||||
protected ?bool $isLocalInstall = null;
|
||||
protected ?array $installerConfig = null;
|
||||
@@ -211,13 +213,14 @@ class Install extends Action
|
||||
}
|
||||
|
||||
// If interactive and web mode enabled, start web server
|
||||
if ($interactive === 'Y' && Console::isInteractive()) {
|
||||
// Skip the web installer when explicit CLI params are provided
|
||||
if ($interactive === 'Y' && Console::isInteractive() && !$this->hasExplicitCliParams()) {
|
||||
Console::success('Starting web installer...');
|
||||
Console::info('Open your browser at: http://localhost:' . InstallerServer::INSTALLER_WEB_PORT);
|
||||
Console::info('Press Ctrl+C to cancel installation');
|
||||
|
||||
$detectedDb = ($existingInstallation && isset($existingDatabase)) ? $existingDatabase : null;
|
||||
$this->startWebServer($defaultHttpPort, $defaultHttpsPort, $organization, $image, $noStart, $vars, $isUpgrade, $detectedDb);
|
||||
$this->startWebServer($defaultHttpPort, $defaultHttpsPort, $organization, $image, $noStart, $vars, $isUpgrade || $existingInstallation, $detectedDb);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -321,7 +324,7 @@ class Install extends Action
|
||||
|
||||
$shouldGenerateSecrets = !$existingInstallation && !$isUpgrade;
|
||||
$input = $this->prepareEnvironmentVariables($userInput, $vars, $shouldGenerateSecrets);
|
||||
$this->performInstallation($httpPort, $httpsPort, $organization, $image, $input, $noStart, null, null, $isUpgrade);
|
||||
$this->performInstallation($httpPort, $httpsPort, $organization, $image, $input, $noStart, null, null, $isUpgrade, migrate: $this->migrate);
|
||||
}
|
||||
|
||||
|
||||
@@ -510,7 +513,9 @@ class Install extends Action
|
||||
?callable $progress = null,
|
||||
?string $resumeFromStep = null,
|
||||
bool $isUpgrade = false,
|
||||
array $account = []
|
||||
array $account = [],
|
||||
?callable $onComplete = null,
|
||||
bool $migrate = false,
|
||||
): void {
|
||||
$isLocalInstall = $this->isLocalInstall();
|
||||
$this->applyLocalPaths($isLocalInstall, false);
|
||||
@@ -633,8 +638,34 @@ class Install extends Action
|
||||
$this->createInitialAdminAccount($account, $progress, $apiUrl, $domain);
|
||||
}
|
||||
|
||||
// Track installs
|
||||
$this->trackSelfHostedInstall($input, $isUpgrade, $version, $account);
|
||||
if ($isUpgrade && $migrate) {
|
||||
// Allow the containers-completed SSE event to flush
|
||||
// before blocking on migration exec
|
||||
usleep(200_000);
|
||||
$currentStep = InstallerServer::STEP_MIGRATION;
|
||||
$this->runDatabaseMigration($progress, $isLocalInstall);
|
||||
} elseif ($isUpgrade) {
|
||||
$this->updateProgress($progress, InstallerServer::STEP_MIGRATION, InstallerServer::STATUS_COMPLETED, messageOverride: 'Migration skipped');
|
||||
}
|
||||
|
||||
// Signal completion before tracking so the SSE stream
|
||||
// finishes and the frontend can redirect immediately.
|
||||
if ($onComplete) {
|
||||
try {
|
||||
$onComplete();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
// Run tracking in a coroutine when inside a Swoole
|
||||
// request so it doesn't block the worker.
|
||||
if (Coroutine::getCid() !== -1) {
|
||||
go(function () use ($input, $isUpgrade, $version, $account) {
|
||||
$this->trackSelfHostedInstall($input, $isUpgrade, $version, $account);
|
||||
});
|
||||
} else {
|
||||
$this->trackSelfHostedInstall($input, $isUpgrade, $version, $account);
|
||||
}
|
||||
|
||||
if ($isCLI) {
|
||||
Console::success('Appwrite installed successfully');
|
||||
@@ -726,6 +757,46 @@ class Install extends Action
|
||||
}
|
||||
}
|
||||
|
||||
private function runDatabaseMigration(?callable $progress, bool $isLocalInstall): void
|
||||
{
|
||||
$this->updateProgress(
|
||||
$progress,
|
||||
InstallerServer::STEP_MIGRATION,
|
||||
InstallerServer::STATUS_IN_PROGRESS,
|
||||
messageOverride: 'Running database migration...'
|
||||
);
|
||||
|
||||
// Allow the SSE chunk to flush before the blocking exec
|
||||
usleep(100_000);
|
||||
|
||||
// Static command — no user input involved
|
||||
$command = $isLocalInstall
|
||||
? 'docker compose exec appwrite migrate 2>&1'
|
||||
: 'docker exec appwrite migrate 2>&1';
|
||||
|
||||
$output = [];
|
||||
\exec($command, $output, $exit);
|
||||
|
||||
if ($exit !== 0) {
|
||||
$message = trim(implode("\n", $output));
|
||||
$this->updateProgress(
|
||||
$progress,
|
||||
InstallerServer::STEP_MIGRATION,
|
||||
InstallerServer::STATUS_ERROR,
|
||||
details: ['output' => $message],
|
||||
messageOverride: 'Migration failed: ' . ($message ?: 'exit code ' . $exit)
|
||||
);
|
||||
throw new \RuntimeException('Database migration failed', 0, $message !== '' ? new \RuntimeException($message) : null);
|
||||
}
|
||||
|
||||
$this->updateProgress(
|
||||
$progress,
|
||||
InstallerServer::STEP_MIGRATION,
|
||||
InstallerServer::STATUS_COMPLETED,
|
||||
messageOverride: 'Database migration completed'
|
||||
);
|
||||
}
|
||||
|
||||
private function trackSelfHostedInstall(array $input, bool $isUpgrade, string $version, array $account): void
|
||||
{
|
||||
if ($this->isLocalInstall()) {
|
||||
@@ -753,7 +824,7 @@ class Install extends Action
|
||||
$name = $account['name'] ?? 'Admin';
|
||||
$email = $account['email'] ?? 'admin@selfhosted.local';
|
||||
|
||||
$hostIp = gethostbyname($domain);
|
||||
$hostIp = @gethostbyname($domain);
|
||||
|
||||
$payload = [
|
||||
'action' => $type,
|
||||
@@ -767,7 +838,7 @@ class Install extends Action
|
||||
'email' => $email,
|
||||
'domain' => $domain,
|
||||
'database' => $database,
|
||||
'hostIp' => $hostIp !== $domain ? $hostIp : null,
|
||||
'ip' => ($hostIp !== false && $hostIp !== $domain) ? $hostIp : null,
|
||||
'os' => php_uname('s') . ' ' . php_uname('r'),
|
||||
'arch' => php_uname('m'),
|
||||
'cpus' => ((int) trim((string) \shell_exec('nproc'))) ?: null,
|
||||
@@ -778,6 +849,8 @@ class Install extends Action
|
||||
try {
|
||||
$client = new Client();
|
||||
$client
|
||||
->setConnectTimeout(5000)
|
||||
->setTimeout(5000)
|
||||
->addHeader('Content-Type', 'application/json')
|
||||
->fetch(self::GROWTH_API_URL . '/analytics', Client::METHOD_POST, $payload);
|
||||
} catch (\Throwable) {
|
||||
@@ -1261,6 +1334,22 @@ class Install extends Action
|
||||
$this->hostPath = $this->getInstallerHostPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any installer-specific CLI params were explicitly passed.
|
||||
* When params like --database or --http-port are provided, the user
|
||||
* intends to run in CLI mode rather than launching the web installer.
|
||||
*/
|
||||
private function hasExplicitCliParams(): bool
|
||||
{
|
||||
$argv = $_SERVER['argv'] ?? [];
|
||||
foreach ($argv as $arg) {
|
||||
if (\str_starts_with($arg, '--') && !\str_starts_with($arg, '--interactive')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the database adapter from a pre-1.9.0 compose file by
|
||||
* checking which DB service exists or reading _APP_DB_HOST.
|
||||
|
||||
@@ -14,16 +14,17 @@ use Appwrite\SDK\Language\Flutter;
|
||||
use Appwrite\SDK\Language\Go;
|
||||
use Appwrite\SDK\Language\GraphQL;
|
||||
use Appwrite\SDK\Language\Kotlin;
|
||||
use Appwrite\SDK\Language\Markdown;
|
||||
use Appwrite\SDK\Language\Node;
|
||||
use Appwrite\SDK\Language\PHP;
|
||||
use Appwrite\SDK\Language\Python;
|
||||
use Appwrite\SDK\Language\ReactNative;
|
||||
use Appwrite\SDK\Language\REST;
|
||||
use Appwrite\SDK\Language\Ruby;
|
||||
use Appwrite\SDK\Language\Rust;
|
||||
use Appwrite\SDK\Language\Swift;
|
||||
use Appwrite\SDK\Language\Web;
|
||||
use Appwrite\SDK\SDK;
|
||||
use Appwrite\Spec\StaticSpec;
|
||||
use Appwrite\Spec\Swagger2;
|
||||
use CzProject\GitPhp\Git;
|
||||
use Utopia\Agents\Adapters\OpenAI;
|
||||
@@ -49,7 +50,10 @@ class SDKs extends Action
|
||||
|
||||
public static function getPlatforms(): array
|
||||
{
|
||||
return Specs::getPlatforms();
|
||||
return [
|
||||
...Specs::getPlatforms(),
|
||||
APP_SDK_PLATFORM_STATIC,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getSdkConfigPath(): string
|
||||
@@ -98,11 +102,14 @@ class SDKs extends Action
|
||||
} else {
|
||||
$sdks = explode(',', $sdks);
|
||||
}
|
||||
$version ??= Console::confirm('Choose an Appwrite version');
|
||||
|
||||
$createRelease = ($release === 'yes');
|
||||
$commitRelease = ($commit === 'yes');
|
||||
|
||||
if ($createRelease && $examplesOnly) {
|
||||
throw new \Exception('Cannot use --release=yes with --mode=examples');
|
||||
}
|
||||
|
||||
if (! $createRelease && ! $examplesOnly) {
|
||||
$git ??= Console::confirm('Should we use git push? (yes/no)');
|
||||
$git = ($git === 'yes');
|
||||
@@ -114,30 +121,34 @@ class SDKs extends Action
|
||||
$prUrls = [];
|
||||
}
|
||||
|
||||
if (! \in_array($version, [
|
||||
'0.6.x',
|
||||
'0.7.x',
|
||||
'0.8.x',
|
||||
'0.9.x',
|
||||
'0.10.x',
|
||||
'0.11.x',
|
||||
'0.12.x',
|
||||
'0.13.x',
|
||||
'0.14.x',
|
||||
'0.15.x',
|
||||
'1.0.x',
|
||||
'1.1.x',
|
||||
'1.2.x',
|
||||
'1.3.x',
|
||||
'1.4.x',
|
||||
'1.5.x',
|
||||
'1.6.x',
|
||||
'1.7.x',
|
||||
'1.8.x',
|
||||
'1.9.x',
|
||||
'latest',
|
||||
])) {
|
||||
throw new \Exception('Unknown version given');
|
||||
if (! $createRelease) {
|
||||
$version ??= Console::confirm('Choose an Appwrite version');
|
||||
|
||||
if (! \in_array($version, [
|
||||
'0.6.x',
|
||||
'0.7.x',
|
||||
'0.8.x',
|
||||
'0.9.x',
|
||||
'0.10.x',
|
||||
'0.11.x',
|
||||
'0.12.x',
|
||||
'0.13.x',
|
||||
'0.14.x',
|
||||
'0.15.x',
|
||||
'1.0.x',
|
||||
'1.1.x',
|
||||
'1.2.x',
|
||||
'1.3.x',
|
||||
'1.4.x',
|
||||
'1.5.x',
|
||||
'1.6.x',
|
||||
'1.7.x',
|
||||
'1.8.x',
|
||||
'1.9.x',
|
||||
'latest',
|
||||
])) {
|
||||
throw new \Exception('Unknown version given');
|
||||
}
|
||||
}
|
||||
|
||||
$selectedPlatforms = ($selectedPlatform === '*' || $selectedPlatform === null) ? null : \array_map('trim', \explode(',', $selectedPlatform));
|
||||
@@ -169,16 +180,140 @@ class SDKs extends Action
|
||||
}
|
||||
|
||||
Console::log('');
|
||||
Console::info("━━━ {$language['name']} SDK ({$platform['name']}, {$version}) ━━━");
|
||||
Console::log(' Fetching API spec...');
|
||||
|
||||
$specPath = __DIR__ . '/../../../../app/config/specs/swagger2-' . $version . '-' . $language['family'] . '.json';
|
||||
if ($createRelease && ! $examplesOnly) {
|
||||
Console::info("━━━ {$language['name']} SDK ({$platform['name']}, {$language['version']}) ━━━");
|
||||
$changelog = $language['changelog'] ?? '';
|
||||
$changelog = ($changelog) ? \file_get_contents($changelog) : '# Change Log';
|
||||
|
||||
if (!file_exists($specPath)) {
|
||||
throw new \Exception('Spec file not found: ' . $specPath . '. Please run "docker compose exec appwrite specs --version=' . $version . '" first to generate the specs.');
|
||||
$repoName = $language['gitUserName'] . '/' . $language['gitRepoName'];
|
||||
$releaseVersion = $language['version'];
|
||||
$releaseNotes = $this->extractReleaseNotes($changelog, $releaseVersion);
|
||||
|
||||
if (empty($releaseNotes)) {
|
||||
$releaseNotes = "Release version {$releaseVersion}";
|
||||
}
|
||||
|
||||
$releaseTitle = $releaseVersion;
|
||||
$releaseTarget = $language['repoBranch'] ?? 'main';
|
||||
|
||||
if ($repoName === '/') {
|
||||
Console::warning(' Not a releasable SDK, skipping');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if release already exists
|
||||
$checkReleaseCommand = 'gh release view ' . \escapeshellarg($releaseVersion) . ' --repo ' . \escapeshellarg($repoName) . ' --json url --jq ".url" 2>/dev/null';
|
||||
$existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? '');
|
||||
|
||||
if (! empty($existingReleaseUrl)) {
|
||||
Console::warning(" Release {$releaseVersion} already exists, skipping");
|
||||
Console::log(" {$existingReleaseUrl}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the latest commit on the target branch already has a release
|
||||
$latestCommitCommand = 'gh api repos/' . $repoName . '/commits/' . $releaseTarget . ' --jq ".sha" 2>/dev/null';
|
||||
$latestCommitSha = trim(\shell_exec($latestCommitCommand) ?? '');
|
||||
|
||||
if (! empty($latestCommitSha)) {
|
||||
$latestReleaseTagCommand = 'gh api repos/' . $repoName . '/releases --jq ".[0] | .tag_name" 2>/dev/null';
|
||||
$latestReleaseTag = trim(\shell_exec($latestReleaseTagCommand) ?? '');
|
||||
|
||||
if (! empty($latestReleaseTag)) {
|
||||
$tagCommitCommand = 'gh api repos/' . $repoName . '/git/ref/tags/' . $latestReleaseTag . ' --jq ".object.sha" 2>/dev/null';
|
||||
$tagCommitSha = trim(\shell_exec($tagCommitCommand) ?? '');
|
||||
|
||||
if (! empty($tagCommitSha) && $latestCommitSha === $tagCommitSha) {
|
||||
Console::warning(" Latest commit already released ({$latestReleaseTag}), skipping");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$previousVersion = '';
|
||||
$tagListCommand = 'gh release list --repo ' . \escapeshellarg($repoName) . ' --limit 1 --json tagName --jq ".[0].tagName" 2>&1';
|
||||
$previousVersion = trim(\shell_exec($tagListCommand) ?? '');
|
||||
|
||||
$formattedNotes = "## What's Changed\n\n";
|
||||
$formattedNotes .= $releaseNotes . "\n\n";
|
||||
|
||||
if (! empty($previousVersion)) {
|
||||
$formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/compare/' . $previousVersion . '...' . $releaseVersion;
|
||||
} else {
|
||||
$formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/releases/tag/' . $releaseVersion;
|
||||
}
|
||||
|
||||
if (! $commitRelease) {
|
||||
Console::info(' [DRY RUN] Would create release:');
|
||||
Console::log(" Repository: {$repoName}");
|
||||
Console::log(" Version: {$releaseVersion}");
|
||||
Console::log(" Title: {$releaseTitle}");
|
||||
Console::log(" Target Branch: {$releaseTarget}");
|
||||
Console::log(' Previous Version: ' . ($previousVersion ?: 'N/A'));
|
||||
Console::log(' Release Notes:');
|
||||
Console::log(' ' . str_replace("\n", "\n ", $formattedNotes));
|
||||
} else {
|
||||
Console::log(" Creating release {$releaseVersion}...");
|
||||
|
||||
$tempNotesFile = \tempnam(\sys_get_temp_dir(), 'release_notes_');
|
||||
\file_put_contents($tempNotesFile, $formattedNotes);
|
||||
|
||||
$releaseCommand = 'gh release create ' . \escapeshellarg($releaseVersion) . ' \
|
||||
--repo ' . \escapeshellarg($repoName) . ' \
|
||||
--title ' . \escapeshellarg($releaseTitle) . ' \
|
||||
--notes-file ' . \escapeshellarg($tempNotesFile) . ' \
|
||||
--target ' . \escapeshellarg($releaseTarget) . ' \
|
||||
2>&1';
|
||||
|
||||
$releaseOutput = [];
|
||||
$releaseReturnCode = 0;
|
||||
\exec($releaseCommand, $releaseOutput, $releaseReturnCode);
|
||||
|
||||
\unlink($tempNotesFile);
|
||||
|
||||
if ($releaseReturnCode === 0) {
|
||||
// Extract release URL from output
|
||||
$releaseUrl = '';
|
||||
foreach ($releaseOutput as $line) {
|
||||
if (strpos($line, 'https://github.com/') !== false) {
|
||||
$releaseUrl = trim($line);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Console::success(" Release {$releaseVersion} created");
|
||||
if (! empty($releaseUrl)) {
|
||||
Console::log(" {$releaseUrl}");
|
||||
}
|
||||
} else {
|
||||
$errorMessage = implode("\n", $releaseOutput);
|
||||
Console::error(" Failed to create release: " . $errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$spec = file_get_contents($specPath);
|
||||
Console::info("━━━ {$language['name']} SDK ({$platform['name']}, {$version}) ━━━");
|
||||
$specFormat = $language['spec'] ?? 'swagger2';
|
||||
$spec = null;
|
||||
if ($specFormat === 'static') {
|
||||
Console::log(' Using static SDK spec...');
|
||||
} else {
|
||||
Console::log(' Fetching API spec...');
|
||||
|
||||
$specPath = __DIR__ . '/../../../../app/config/specs/swagger2-' . $version . '-' . $language['family'] . '.json';
|
||||
|
||||
if (!file_exists($specPath)) {
|
||||
throw new \Exception('Spec file not found: ' . $specPath . '. Please run "docker compose exec appwrite specs --version=' . $version . '" first to generate the specs.');
|
||||
}
|
||||
|
||||
$spec = file_get_contents($specPath);
|
||||
}
|
||||
|
||||
$cover = 'https://github.com/appwrite/appwrite/raw/main/public/images/github.png';
|
||||
$result = \realpath(__DIR__ . '/../../../../app') . '/sdks/' . $key . '-' . $language['key'];
|
||||
@@ -301,16 +436,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
$config = new Kotlin();
|
||||
$warning = $warning . "\n\n > This is the Kotlin SDK for integrating with Appwrite from your Kotlin server-side code. If you're looking for the Android SDK you should check [appwrite/sdk-for-android](https://github.com/appwrite/sdk-for-android)";
|
||||
break;
|
||||
case 'rust':
|
||||
$config = new Rust();
|
||||
break;
|
||||
case 'graphql':
|
||||
$config = new GraphQL();
|
||||
break;
|
||||
case 'rest':
|
||||
$config = new REST();
|
||||
break;
|
||||
case 'markdown':
|
||||
$config = new Markdown();
|
||||
$config->setNPMPackage('@appwrite.io/docs');
|
||||
break;
|
||||
case 'agent-skills':
|
||||
$config = new AgentSkills();
|
||||
break;
|
||||
@@ -321,124 +455,22 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
throw new \Exception('Language "' . $language['key'] . '" not supported');
|
||||
}
|
||||
|
||||
if ($createRelease && ! $examplesOnly) {
|
||||
$repoName = $language['gitUserName'] . '/' . $language['gitRepoName'];
|
||||
$releaseVersion = $language['version'];
|
||||
$releaseNotes = $this->extractReleaseNotes($changelog, $releaseVersion);
|
||||
|
||||
if (empty($releaseNotes)) {
|
||||
$releaseNotes = "Release version {$releaseVersion}";
|
||||
}
|
||||
|
||||
$releaseTitle = $releaseVersion;
|
||||
$releaseTarget = $language['repoBranch'] ?? 'main';
|
||||
|
||||
if ($repoName === '/') {
|
||||
Console::warning(' Not a releasable SDK, skipping');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if release already exists
|
||||
$checkReleaseCommand = 'gh release view ' . \escapeshellarg($releaseVersion) . ' --repo ' . \escapeshellarg($repoName) . ' --json url --jq ".url" 2>/dev/null';
|
||||
$existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? '');
|
||||
|
||||
if (! empty($existingReleaseUrl)) {
|
||||
Console::warning(" Release {$releaseVersion} already exists, skipping");
|
||||
Console::log(" {$existingReleaseUrl}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the latest commit on the target branch already has a release
|
||||
$latestCommitCommand = 'gh api repos/' . $repoName . '/commits/' . $releaseTarget . ' --jq ".sha" 2>/dev/null';
|
||||
$latestCommitSha = trim(\shell_exec($latestCommitCommand) ?? '');
|
||||
|
||||
if (! empty($latestCommitSha)) {
|
||||
$latestReleaseTagCommand = 'gh api repos/' . $repoName . '/releases --jq ".[0] | .tag_name" 2>/dev/null';
|
||||
$latestReleaseTag = trim(\shell_exec($latestReleaseTagCommand) ?? '');
|
||||
|
||||
if (! empty($latestReleaseTag)) {
|
||||
$tagCommitCommand = 'gh api repos/' . $repoName . '/git/ref/tags/' . $latestReleaseTag . ' --jq ".object.sha" 2>/dev/null';
|
||||
$tagCommitSha = trim(\shell_exec($tagCommitCommand) ?? '');
|
||||
|
||||
if (! empty($tagCommitSha) && $latestCommitSha === $tagCommitSha) {
|
||||
Console::warning(" Latest commit already released ({$latestReleaseTag}), skipping");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$previousVersion = '';
|
||||
$tagListCommand = 'gh release list --repo ' . \escapeshellarg($repoName) . ' --limit 1 --json tagName --jq ".[0].tagName" 2>&1';
|
||||
$previousVersion = trim(\shell_exec($tagListCommand) ?? '');
|
||||
|
||||
$formattedNotes = "## What's Changed\n\n";
|
||||
$formattedNotes .= $releaseNotes . "\n\n";
|
||||
|
||||
if (! empty($previousVersion)) {
|
||||
$formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/compare/' . $previousVersion . '...' . $releaseVersion;
|
||||
} else {
|
||||
$formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/releases/tag/' . $releaseVersion;
|
||||
}
|
||||
|
||||
if (! $commitRelease) {
|
||||
Console::info(' [DRY RUN] Would create release:');
|
||||
Console::log(" Repository: {$repoName}");
|
||||
Console::log(" Version: {$releaseVersion}");
|
||||
Console::log(" Title: {$releaseTitle}");
|
||||
Console::log(" Target Branch: {$releaseTarget}");
|
||||
Console::log(' Previous Version: ' . ($previousVersion ?: 'N/A'));
|
||||
Console::log(' Release Notes:');
|
||||
Console::log(' ' . str_replace("\n", "\n ", $formattedNotes));
|
||||
} else {
|
||||
Console::log(" Creating release {$releaseVersion}...");
|
||||
|
||||
$tempNotesFile = \tempnam(\sys_get_temp_dir(), 'release_notes_');
|
||||
\file_put_contents($tempNotesFile, $formattedNotes);
|
||||
|
||||
$releaseCommand = 'gh release create ' . \escapeshellarg($releaseVersion) . ' \
|
||||
--repo ' . \escapeshellarg($repoName) . ' \
|
||||
--title ' . \escapeshellarg($releaseTitle) . ' \
|
||||
--notes-file ' . \escapeshellarg($tempNotesFile) . ' \
|
||||
--target ' . \escapeshellarg($releaseTarget) . ' \
|
||||
2>&1';
|
||||
|
||||
$releaseOutput = [];
|
||||
$releaseReturnCode = 0;
|
||||
\exec($releaseCommand, $releaseOutput, $releaseReturnCode);
|
||||
|
||||
\unlink($tempNotesFile);
|
||||
|
||||
if ($releaseReturnCode === 0) {
|
||||
// Extract release URL from output
|
||||
$releaseUrl = '';
|
||||
foreach ($releaseOutput as $line) {
|
||||
if (strpos($line, 'https://github.com/') !== false) {
|
||||
$releaseUrl = trim($line);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Console::success(" Release {$releaseVersion} created");
|
||||
if (! empty($releaseUrl)) {
|
||||
Console::log(" {$releaseUrl}");
|
||||
}
|
||||
} else {
|
||||
$errorMessage = implode("\n", $releaseOutput);
|
||||
Console::error(" Failed to create release: " . $errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Console::log($examplesOnly
|
||||
? ' Generating examples...'
|
||||
: ' Generating SDK...');
|
||||
|
||||
$sdk = new SDK($config, new Swagger2($spec));
|
||||
$sdk = new SDK(
|
||||
$config,
|
||||
$specFormat === 'static'
|
||||
? new StaticSpec(
|
||||
title: 'Appwrite',
|
||||
description: 'Appwrite backend as a service',
|
||||
version: $version,
|
||||
licenseName: 'BSD-3-Clause',
|
||||
licenseURL: 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE',
|
||||
)
|
||||
: new Swagger2($spec)
|
||||
);
|
||||
|
||||
$sdk
|
||||
->setName($language['name'])
|
||||
@@ -479,6 +511,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
|
||||
try {
|
||||
$sdk->generate($result);
|
||||
Console::success($examplesOnly
|
||||
? " Examples generated at {$result}"
|
||||
: " SDK generated at {$result}");
|
||||
} catch (\Throwable $exception) {
|
||||
Console::error($exception->getMessage());
|
||||
}
|
||||
|
||||
@@ -347,6 +347,13 @@ class Specs extends Action
|
||||
$keys = $this->getKeys();
|
||||
|
||||
$generatedFiles = [];
|
||||
$endpoint = System::getEnv('_APP_HOME', 'https://appwrite.io');
|
||||
$email = System::getEnv('_APP_SYSTEM_TEAM_EMAIL', 'team@appwrite.io');
|
||||
$specsDir = __DIR__ . '/../../../../app/config/specs';
|
||||
|
||||
if (!is_dir($specsDir) && !@mkdir($specsDir, 0755, true) && !is_dir($specsDir)) {
|
||||
throw new Exception('Failed to create specs directory: ' . $specsDir);
|
||||
}
|
||||
|
||||
foreach ($platforms as $platform) {
|
||||
$routes = [];
|
||||
@@ -443,8 +450,6 @@ class Specs extends Action
|
||||
foreach (['swagger2', 'open-api3'] as $format) {
|
||||
$formatInstance = $this->getFormatInstance($format, $arguments);
|
||||
$specs = new Specification($formatInstance);
|
||||
$endpoint = System::getEnv('_APP_HOME', '[HOSTNAME]');
|
||||
$email = System::getEnv('_APP_SYSTEM_TEAM_EMAIL', APP_EMAIL_TEAM);
|
||||
|
||||
$formatInstance
|
||||
->setParam('name', APP_NAME)
|
||||
@@ -463,36 +468,30 @@ class Specs extends Action
|
||||
->setParam('docs.description', 'Full API docs, specs and tutorials')
|
||||
->setParam('docs.url', $endpoint . '/docs');
|
||||
|
||||
$specsDir = __DIR__ . '/../../../../app/config/specs';
|
||||
$path = $mocks
|
||||
? $specsDir . '/' . $format . '-mocks-' . $platform . '.json'
|
||||
: $specsDir . '/' . $format . '-' . $version . '-' . $platform . '.json';
|
||||
|
||||
if (!is_dir($specsDir)) {
|
||||
if (!mkdir($specsDir, 0755, true)) {
|
||||
throw new Exception('Failed to create specs directory: ' . $specsDir);
|
||||
}
|
||||
$parsedSpecs = $specs->parse();
|
||||
$encodedSpecs = \json_encode($parsedSpecs, JSON_PRETTY_PRINT);
|
||||
|
||||
unset($parsedSpecs);
|
||||
|
||||
if ($encodedSpecs === false) {
|
||||
throw new Exception('Failed to encode ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . \json_last_error_msg());
|
||||
}
|
||||
|
||||
if ($mocks) {
|
||||
$path = $specsDir . '/' . $format . '-mocks-' . $platform . '.json';
|
||||
|
||||
if (!file_put_contents($path, json_encode($specs->parse(), JSON_PRETTY_PRINT))) {
|
||||
throw new Exception('Failed to save mocks spec file: ' . $path);
|
||||
}
|
||||
|
||||
$generatedFiles[] = realpath($path);
|
||||
Console::success('Saved mocks spec file: ' . realpath($path));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $specsDir . '/' . $format . '-' . $version . '-' . $platform . '.json';
|
||||
|
||||
if (!file_put_contents($path, json_encode($specs->parse(), JSON_PRETTY_PRINT))) {
|
||||
throw new Exception('Failed to save spec file: ' . $path);
|
||||
if (\file_put_contents($path, $encodedSpecs) === false) {
|
||||
throw new Exception('Failed to save ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . $path);
|
||||
}
|
||||
|
||||
$generatedFiles[] = realpath($path);
|
||||
Console::success('Saved spec file: ' . realpath($path));
|
||||
Console::success('Saved ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . realpath($path));
|
||||
|
||||
unset($encodedSpecs, $specs, $formatInstance);
|
||||
}
|
||||
|
||||
unset($arguments, $models, $routes, $services);
|
||||
}
|
||||
|
||||
if ($git === 'yes') {
|
||||
|
||||
@@ -30,6 +30,7 @@ class Upgrade extends Install
|
||||
->param('interactive', 'Y', new Text(1), 'Run an interactive session', true)
|
||||
->param('no-start', false, new Boolean(true), 'Run an interactive session', true)
|
||||
->param('database', 'mongodb', new Text(length: 0), 'Database to use (mongodb|mariadb|postgresql)', true)
|
||||
->param('migrate', true, new Boolean(true), 'Run database migration after upgrade', true)
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -40,9 +41,11 @@ class Upgrade extends Install
|
||||
string $image,
|
||||
string $interactive,
|
||||
bool $noStart,
|
||||
string $database
|
||||
string $database,
|
||||
bool $migrate = true,
|
||||
): void {
|
||||
$this->isUpgrade = true;
|
||||
$this->migrate = $migrate;
|
||||
$isLocalInstall = $this->isLocalInstall();
|
||||
$this->applyLocalPaths($isLocalInstall, true);
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ class User extends Document
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isPrivileged(array $roles): bool
|
||||
public function isPrivileged(array $roles): bool
|
||||
{
|
||||
if (
|
||||
in_array(self::ROLE_OWNER, $roles) ||
|
||||
@@ -122,7 +122,7 @@ class User extends Document
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isApp(array $roles): bool
|
||||
public function isApp(array $roles): bool
|
||||
{
|
||||
if (in_array(self::ROLE_APPS, $roles)) {
|
||||
return true;
|
||||
@@ -175,7 +175,5 @@ class User extends Document
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ class Request extends UtopiaRequest
|
||||
$forwardedUserAgent = $this->getHeader('x-forwarded-user-agent');
|
||||
if (!empty($forwardedUserAgent)) {
|
||||
$roles = $this->authorization->getRoles();
|
||||
$isAppUser = User::isApp($roles);
|
||||
$isAppUser = $this->user?->isApp($roles) ?? false;
|
||||
|
||||
if ($isAppUser) {
|
||||
return $forwardedUserAgent;
|
||||
@@ -239,9 +239,15 @@ class Request extends UtopiaRequest
|
||||
}
|
||||
|
||||
private ?Authorization $authorization = null;
|
||||
private ?User $user = null;
|
||||
|
||||
public function setAuthorization(Authorization $authorization): void
|
||||
{
|
||||
$this->authorization = $authorization;
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,8 +505,9 @@ class Response extends SwooleResponse
|
||||
|
||||
if ($rule['sensitive']) {
|
||||
$roles = $this->authorization->getRoles();
|
||||
$isPrivilegedUser = DBUser::isPrivileged($roles);
|
||||
$isAppUser = DBUser::isApp($roles);
|
||||
$user = $this->user ?? new DBUser();
|
||||
$isPrivilegedUser = $user->isPrivileged($roles);
|
||||
$isAppUser = $user->isApp($roles);
|
||||
|
||||
if ((!$isPrivilegedUser && !$isAppUser) && !self::$showSensitive) {
|
||||
$data->setAttribute($key, '');
|
||||
@@ -674,9 +675,15 @@ class Response extends SwooleResponse
|
||||
}
|
||||
|
||||
private ?Authorization $authorization = null;
|
||||
private ?DBUser $user = null;
|
||||
|
||||
public function setAuthorization(Authorization $authorization): void
|
||||
{
|
||||
$this->authorization = $authorization;
|
||||
}
|
||||
|
||||
public function setUser(DBUser $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ class MFAType extends Model
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Secret token used for TOTP factor.',
|
||||
'default' => '',
|
||||
'example' => true
|
||||
'example' => '[SHARED_SECRET]'
|
||||
])
|
||||
->addRule('uri', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'URI for authenticator apps.',
|
||||
'default' => '',
|
||||
'example' => true
|
||||
'example' => 'otpauth://totp/appwrite:user@example.com?secret=[SHARED_SECRET]&issuer=appwrite'
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
@@ -3631,7 +3631,7 @@ trait DatabasesBase
|
||||
]);
|
||||
|
||||
$adapter = getenv('_APP_DB_ADAPTER');
|
||||
if ($adapter === 'mongodb') {
|
||||
if ($adapter === 'mongodb' || !$this->getSupportForAttributes()) {
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
} else {
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
@@ -134,7 +134,7 @@ class ModuleTest extends TestCase
|
||||
$this->assertActionParams($action, [
|
||||
'appDomain', 'httpPort', 'httpsPort', 'emailCertificates', 'opensslKey',
|
||||
'assistantOpenAIKey', 'accountEmail', 'accountPassword', 'database',
|
||||
'installId', 'retryStep',
|
||||
'installId', 'retryStep', 'migrate',
|
||||
]);
|
||||
$this->assertActionInjects($action, ['request', 'response', 'swooleResponse', 'installerState', 'installerConfig', 'installerPaths']);
|
||||
}
|
||||
|
||||
@@ -171,36 +171,40 @@ class UserTest extends TestCase
|
||||
|
||||
public function testIsPrivilegedUser(): void
|
||||
{
|
||||
$this->assertEquals(false, User::isPrivileged([]));
|
||||
$this->assertEquals(false, User::isPrivileged([Role::guests()->toString()]));
|
||||
$this->assertEquals(false, User::isPrivileged([Role::users()->toString()]));
|
||||
$this->assertEquals(true, User::isPrivileged([User::ROLE_ADMIN]));
|
||||
$this->assertEquals(true, User::isPrivileged([User::ROLE_DEVELOPER]));
|
||||
$this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER]));
|
||||
$this->assertEquals(false, User::isPrivileged([User::ROLE_APPS]));
|
||||
$this->assertEquals(false, User::isPrivileged([User::ROLE_SYSTEM]));
|
||||
$user = new User();
|
||||
|
||||
$this->assertEquals(false, User::isPrivileged([User::ROLE_APPS, User::ROLE_APPS]));
|
||||
$this->assertEquals(false, User::isPrivileged([User::ROLE_APPS, Role::guests()->toString()]));
|
||||
$this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER, Role::guests()->toString()]));
|
||||
$this->assertEquals(true, User::isPrivileged([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER]));
|
||||
$this->assertEquals(false, $user->isPrivileged([]));
|
||||
$this->assertEquals(false, $user->isPrivileged([Role::guests()->toString()]));
|
||||
$this->assertEquals(false, $user->isPrivileged([Role::users()->toString()]));
|
||||
$this->assertEquals(true, $user->isPrivileged([User::ROLE_ADMIN]));
|
||||
$this->assertEquals(true, $user->isPrivileged([User::ROLE_DEVELOPER]));
|
||||
$this->assertEquals(true, $user->isPrivileged([User::ROLE_OWNER]));
|
||||
$this->assertEquals(false, $user->isPrivileged([User::ROLE_APPS]));
|
||||
$this->assertEquals(false, $user->isPrivileged([User::ROLE_SYSTEM]));
|
||||
|
||||
$this->assertEquals(false, $user->isPrivileged([User::ROLE_APPS, User::ROLE_APPS]));
|
||||
$this->assertEquals(false, $user->isPrivileged([User::ROLE_APPS, Role::guests()->toString()]));
|
||||
$this->assertEquals(true, $user->isPrivileged([User::ROLE_OWNER, Role::guests()->toString()]));
|
||||
$this->assertEquals(true, $user->isPrivileged([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER]));
|
||||
}
|
||||
|
||||
public function testIsAppUser(): void
|
||||
{
|
||||
$this->assertEquals(false, User::isApp([]));
|
||||
$this->assertEquals(false, User::isApp([Role::guests()->toString()]));
|
||||
$this->assertEquals(false, User::isApp([Role::users()->toString()]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_ADMIN]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_DEVELOPER]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_OWNER]));
|
||||
$this->assertEquals(true, User::isApp([User::ROLE_APPS]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_SYSTEM]));
|
||||
$user = new User();
|
||||
|
||||
$this->assertEquals(true, User::isApp([User::ROLE_APPS, User::ROLE_APPS]));
|
||||
$this->assertEquals(true, User::isApp([User::ROLE_APPS, Role::guests()->toString()]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_OWNER, Role::guests()->toString()]));
|
||||
$this->assertEquals(false, User::isApp([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER]));
|
||||
$this->assertEquals(false, $user->isApp([]));
|
||||
$this->assertEquals(false, $user->isApp([Role::guests()->toString()]));
|
||||
$this->assertEquals(false, $user->isApp([Role::users()->toString()]));
|
||||
$this->assertEquals(false, $user->isApp([User::ROLE_ADMIN]));
|
||||
$this->assertEquals(false, $user->isApp([User::ROLE_DEVELOPER]));
|
||||
$this->assertEquals(false, $user->isApp([User::ROLE_OWNER]));
|
||||
$this->assertEquals(true, $user->isApp([User::ROLE_APPS]));
|
||||
$this->assertEquals(false, $user->isApp([User::ROLE_SYSTEM]));
|
||||
|
||||
$this->assertEquals(true, $user->isApp([User::ROLE_APPS, User::ROLE_APPS]));
|
||||
$this->assertEquals(true, $user->isApp([User::ROLE_APPS, Role::guests()->toString()]));
|
||||
$this->assertEquals(false, $user->isApp([User::ROLE_OWNER, Role::guests()->toString()]));
|
||||
$this->assertEquals(false, $user->isApp([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER]));
|
||||
}
|
||||
|
||||
public function testGuestRoles(): void
|
||||
|
||||
Reference in New Issue
Block a user