Compare commits

...
Author SHA1 Message Date
DarshanandGitHub a66b565150 Merge branch '1.8.x' into optional-assistant 2025-12-31 19:21:37 +05:30
Darshan ff160871ae update: check assistant status on upgrade as well. 2025-12-31 19:21:19 +05:30
Darshan 92a3eca194 Merge branch '1.8.x' into optional-assistant 2025-12-31 19:09:54 +05:30
Shmuel FogelandGitHub 07135956df Merge pull request #11053 from appwrite/skip-migrations-shutdown
skip migration shutdown
2025-12-31 14:27:41 +02:00
fogelito b5a363a2a7 skip shutdown 2025-12-31 14:01:56 +02:00
Matej BačoandGitHub 31dc9ff873 Merge pull request #11052 from appwrite/chore-upgrade-vcs-version
Chore: Upgrade VCS lib with getCommit fix
2025-12-31 12:22:11 +01:00
Matej Bačo 408f9c2d39 Upgrade VCS with getCommit fix 2025-12-31 11:54:22 +01:00
DarshanandGitHub dfd5965d25 Merge pull request #11051 from appwrite/fix-resources 2025-12-31 15:25:55 +05:30
Darshan 2a38eedc98 fix: flaky test!
fix: get resource report what we want.
2025-12-31 15:00:23 +05:30
DarshanandGitHub f875b965be Merge pull request #11049 from appwrite/console-module 2025-12-31 13:20:24 +05:30
Darshan 2e57e5a868 fix: url. 2025-12-31 11:59:01 +05:30
Darshan 69ce99d1b4 feat: console module. 2025-12-31 11:52:41 +05:30
Chirag AggarwalandGitHub 6b881d4aaf Merge pull request #11045 from appwrite/reduce-sdk-release-steps 2025-12-31 10:01:11 +05:30
Damodar LohaniandGitHub aad7d6dbca Merge pull request #10953 from appwrite/feat-audits-upgrade
Feat: Audits  upgrade
2025-12-31 06:55:34 +05:45
Chirag Aggarwal e3412bc554 chore: reduce sdk release steps 2025-12-30 18:25:11 +05:30
HemachandarandGitHub c1e50c7abd Write to new resource attributes in keys (#11003)
* Write to new resource attributes in `keys`

* temp for tests

* list keys

* add subqueries

* lint
2025-12-30 17:09:59 +05:30
Luke B. SilverandGitHub 6499f436ca Merge pull request #11040 from appwrite/fix-task-subprocesses
fix: task subprocesses
2025-12-30 09:37:49 +00:00
Damodar LohaniandGitHub 15dc316535 Merge branch '1.8.x' into feat-audits-upgrade 2025-12-30 07:44:10 +05:45
Damodar LohaniandGitHub a7724028b3 Merge branch '1.8.x' into feat-audits-upgrade 2025-12-29 13:23:20 +05:45
Damodar LohaniandGitHub 419d56b4b1 Merge branch '1.8.x' into feat-audits-upgrade 2025-12-28 14:58:36 +05:45
Damodar Lohani d2cda9770b Use get audit resource for audit cleanup. 2025-12-28 09:01:37 +00:00
Damodar Lohani 69430154e6 update autit to rc version 2025-12-24 10:24:51 +00:00
Damodar LohaniandGitHub 0601305545 Merge branch '1.8.x' into feat-audits-upgrade 2025-12-24 15:21:49 +05:45
Damodar Lohani 5519086c29 Use db 3.x audit 2025-12-24 01:30:02 +00:00
Damodar Lohani 33b9f33f44 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into feat-audits-upgrade 2025-12-24 01:26:49 +00:00
Damodar Lohani 8baa5cb659 Merge remote-tracking branch 'origin/1.8.x' into feat-audits-upgrade 2025-12-17 08:59:05 +00:00
Damodar LohaniandGitHub cec5cd7bdc Merge branch '1.8.x' into feat-audits-upgrade 2025-12-15 09:44:52 +05:45
Damodar Lohani 63c2d72b2e Fix logs 2025-12-15 03:58:00 +00:00
Damodar Lohani 801219374c Fix queries 2025-12-15 02:50:21 +00:00
Damodar Lohani dc3459edd0 remove dump 2025-12-15 02:43:27 +00:00
Damodar Lohani ace9d86744 fix attribute 2025-12-15 02:41:56 +00:00
Damodar Lohani 2d3e34fd5a fix namespace 2025-12-15 02:20:10 +00:00
Damodar Lohani 69ad4ae930 Fix format 2025-12-15 02:15:45 +00:00
Damodar Lohani 0037305e16 Fix: failing tests 2025-12-15 02:12:35 +00:00
Damodar Lohani 1a205c3f3b upgrade audit 2025-12-14 08:50:39 +00:00
Damodar Lohani 99966b5a24 upgrade audit 2025-12-14 08:35:40 +00:00
Damodar Lohani 2437d2be77 Upgrade audit and fix 2025-12-14 08:30:22 +00:00
Damodar Lohani 2043470bd0 fix typo 2025-12-14 08:00:31 +00:00
Damodar Lohani 667285d536 fix tyupe 2025-12-14 07:58:16 +00:00
Damodar Lohani 1199c1fc52 fix typo 2025-12-14 07:57:37 +00:00
Damodar Lohani f270e47b48 more fixes to audit queries 2025-12-14 07:50:21 +00:00
Darshan 5678860d13 add: optional support for assistant. 2025-12-14 13:07:26 +05:30
Damodar Lohani b83125a41e Fix audits creation 2025-12-14 07:22:39 +00:00
Damodar Lohani 2dcb131778 fix collection name 2025-12-14 02:34:33 +00:00
Damodar Lohani 30083598c6 fix audit 2025-12-14 02:33:17 +00:00
Damodar Lohani a0599d2658 Upgrade audit 2025-12-14 02:31:24 +00:00
Damodar Lohani c50db111d6 format 2025-12-14 02:19:53 +00:00
Damodar Lohani 9441295510 Feat: Audits upgrade 2025-12-14 01:43:35 +00:00
43 changed files with 888 additions and 366 deletions
+69 -1
View File
@@ -6,7 +6,7 @@ use Utopia\Database\Helpers\ID;
$providers = Config::getParam('oAuthProviders', []);
return [
$platformCollections = [
'projects' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('projects'),
@@ -643,6 +643,39 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => 'resourceType',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'resourceId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'resourceInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
@@ -718,6 +751,13 @@ return [
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_resource',
'type' => Database::INDEX_KEY,
'attributes' => ['resourceType', 'resourceInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_accessedAt',
'type' => Database::INDEX_KEY,
@@ -1903,3 +1943,31 @@ return [
'indexes' => []
],
];
// Organization API keys subquery
$platformCollections['teams']['attributes'][] = [
'$id' => ID::custom('keys'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['subQueryOrganizationKeys'],
];
// Account API keys subquery
$platformCollections['users']['attributes'][] = [
'$id' => ID::custom('keys'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['subQueryAccountKeys'],
];
return $platformCollections;
+3 -3
View File
@@ -20,7 +20,7 @@ return [
'name' => 'Console',
'subtitle' => '',
'description' => '',
'controller' => 'web/console.php',
'controller' => '', // Uses modules
'sdk' => false,
'docs' => false,
'docsUrl' => '',
@@ -270,9 +270,9 @@ return [
'console' => [
'key' => 'console',
'name' => 'Console',
'subtitle' => 'The Console service allows you to interact with console relevant informations.',
'subtitle' => 'The Console service allows you to interact with console relevant information.',
'description' => '',
'controller' => 'api/console.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => '',
+8 -6
View File
@@ -37,7 +37,7 @@ use libphonenumber\PhoneNumberUtil;
use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Audit\Audit as EventAudit;
use Utopia\Audit\Audit;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Code as ProofsCode;
use Utopia\Auth\Proofs\Password as ProofsPassword;
@@ -3033,7 +3033,8 @@ App::get('/v1/account/logs')
->inject('locale')
->inject('geodb')
->inject('dbForProject')
->action(function (array $queries, bool $includeTotal, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject) {
->inject('audit')
->action(function (array $queries, bool $includeTotal, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject, Audit $audit) {
try {
$queries = Query::parseQueries($queries);
@@ -3041,9 +3042,10 @@ App::get('/v1/account/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$audit = new EventAudit($dbForProject);
$logs = $audit->getLogsByUser($user->getSequence(), $queries);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$logs = $audit->getLogsByUser($user->getSequence(), offset: $offset, limit: $limit);
$output = [];
@@ -3072,7 +3074,7 @@ App::get('/v1/account/logs')
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByUser($user->getSequence(), $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByUser($user->getSequence()) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
-147
View File
@@ -1,147 +0,0 @@
<?php
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Document;
use Utopia\Domains\Domain;
use Utopia\System\System;
use Utopia\Validator\IP;
use Utopia\Validator\Text;
App::init()
->groups(['console'])
->inject('project')
->action(function (Document $project) {
if ($project->getId() !== 'console') {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN);
}
});
App::get('/v1/console/variables')
->desc('Get variables')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'console',
group: 'console',
name: 'variables',
description: '/docs/references/console/variables.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_CONSOLE_VARIABLES,
)
],
contentType: ContentType::JSON
))
->inject('response')
->action(function (Response $response) {
$validator = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME'));
$isCNAMEValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')) && $validator->isKnown() && !$validator->isTest();
$validator = new IP(IP::V4);
$isAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_A', '')) && ($validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_A')));
$validator = new IP(IP::V6);
$isAAAAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_AAAA', '')) && $validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA'));
$isDomainEnabled = $isAAAAValid || $isAValid || $isCNAMEValid;
$isVcsEnabled = !empty(System::getEnv('_APP_VCS_GITHUB_APP_NAME', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_APP_ID', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''));
$isAssistantEnabled = !empty(System::getEnv('_APP_ASSISTANT_OPENAI_API_KEY', ''));
$variables = new Document([
'_APP_DOMAIN_TARGET_CNAME' => System::getEnv('_APP_DOMAIN_TARGET_CNAME'),
'_APP_DOMAIN_TARGET_AAAA' => System::getEnv('_APP_DOMAIN_TARGET_AAAA'),
'_APP_DOMAIN_TARGET_A' => System::getEnv('_APP_DOMAIN_TARGET_A'),
// Combine CAA domain with most common flags and tag (no parameters)
'_APP_DOMAIN_TARGET_CAA' => '0 issue "' . System::getEnv('_APP_DOMAIN_TARGET_CAA') . '"',
'_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'),
'_APP_COMPUTE_BUILD_TIMEOUT' => +System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT'),
'_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'),
'_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'),
'_APP_VCS_ENABLED' => $isVcsEnabled,
'_APP_DOMAIN_ENABLED' => $isDomainEnabled,
'_APP_ASSISTANT_ENABLED' => $isAssistantEnabled,
'_APP_DOMAIN_SITES' => System::getEnv('_APP_DOMAIN_SITES'),
'_APP_DOMAIN_FUNCTIONS' => System::getEnv('_APP_DOMAIN_FUNCTIONS'),
'_APP_OPTIONS_FORCE_HTTPS' => System::getEnv('_APP_OPTIONS_FORCE_HTTPS'),
'_APP_DOMAINS_NAMESERVERS' => System::getEnv('_APP_DOMAINS_NAMESERVERS'),
]);
$response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES);
});
App::post('/v1/console/assistant')
->desc('Create assistant query')
->groups(['api', 'assistant'])
->label('scope', 'assistant.read')
->label('sdk', new Method(
namespace: 'assistant',
group: 'console',
name: 'chat',
description: '/docs/references/assistant/chat.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::TEXT
))
->label('abuse-limit', 15)
->label('abuse-key', 'userId:{userId}')
->param('prompt', '', new Text(2000), 'Prompt. A string containing questions asked to the AI assistant.')
->inject('response')
->action(function (string $prompt, Response $response) {
$ch = curl_init('http://appwrite-assistant:3003/v1/models/assistant/prompt');
$responseHeaders = [];
$query = json_encode(['prompt' => $prompt]);
$headers = ['accept: text/event-stream'];
$handleEvent = function ($ch, $data) use ($response) {
$response->chunk($data);
return \strlen($data);
};
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $handleEvent);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 9000);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
$len = strlen($header);
$header = explode(':', $header, 2);
if (count($header) < 2) { // ignore invalid headers
return $len;
}
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
return $len;
});
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
curl_exec($ch);
curl_close($ch);
$response->chunk('', true);
});
+32 -16
View File
@@ -1145,7 +1145,8 @@ App::get('/v1/messaging/providers/:providerId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $providerId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $providerId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
@@ -1158,9 +1159,12 @@ App::get('/v1/messaging/providers/:providerId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$audit = new Audit($dbForProject);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$resource = 'provider/' . $providerId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, offset: $offset, limit: $limit);
$output = [];
foreach ($logs as $i => &$log) {
@@ -1207,7 +1211,7 @@ App::get('/v1/messaging/providers/:providerId/logs')
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -2549,7 +2553,8 @@ App::get('/v1/messaging/topics/:topicId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $topicId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $topicId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$topic = $dbForProject->getDocument('topics', $topicId);
if ($topic->isEmpty()) {
@@ -2562,9 +2567,12 @@ App::get('/v1/messaging/topics/:topicId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$audit = new Audit($dbForProject);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$resource = 'topic/' . $topicId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, offset: $offset, limit: $limit);
$output = [];
@@ -2612,7 +2620,7 @@ App::get('/v1/messaging/topics/:topicId/logs')
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -2966,7 +2974,8 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $subscriberId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $subscriberId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$subscriber = $dbForProject->getDocument('subscribers', $subscriberId);
if ($subscriber->isEmpty()) {
@@ -2979,9 +2988,12 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$audit = new Audit($dbForProject);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$resource = 'subscriber/' . $subscriberId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, limit: $limit, offset: $offset);
$output = [];
@@ -3029,7 +3041,7 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -3762,7 +3774,8 @@ App::get('/v1/messaging/messages/:messageId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $messageId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $messageId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3775,9 +3788,12 @@ App::get('/v1/messaging/messages/:messageId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$audit = new Audit($dbForProject);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$resource = 'message/' . $messageId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, limit: $limit, offset: $offset);
$output = [];
@@ -3825,7 +3841,7 @@ App::get('/v1/messaging/messages/:messageId/logs')
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
+37 -7
View File
@@ -21,6 +21,7 @@ use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\App;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
@@ -247,13 +248,15 @@ App::post('/v1/projects')
}
if ($create || $projectTables) {
$audit = new Audit($dbForProject);
$adapter = new AdapterDatabase($dbForProject);
$audit = new Audit($adapter);
$audit->setup();
}
if (!$create && $sharedTablesV1) {
$attributes = \array_map(fn ($attribute) => new Document($attribute), Audit::ATTRIBUTES);
$indexes = \array_map(fn (array $index) => new Document($index), Audit::INDEXES);
$adapter = new AdapterDatabase($dbForProject);
$attributes = $adapter->getAttributeDocuments();
$indexes = $adapter->getIndexDocuments();
$dbForProject->createDocument(Database::METADATA, new Document([
'$id' => ID::custom('audit'),
'$permissions' => [Permission::create(Role::any())],
@@ -1499,6 +1502,9 @@ App::post('/v1/projects/:projectId/keys')
],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'resourceInternalId' => $project->getSequence(),
'resourceId' => $project->getId(),
'resourceType' => 'projects',
'name' => $name,
'scopes' => $scopes,
'expire' => $expire,
@@ -1546,7 +1552,13 @@ App::get('/v1/projects/:projectId/keys')
}
$keys = $dbForPlatform->find('keys', [
Query::equal('projectInternalId', [$project->getSequence()]),
Query::or([
Query::equal('projectInternalId', [$project->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
])
]),
Query::limit(5000),
]);
@@ -1587,7 +1599,13 @@ App::get('/v1/projects/:projectId/keys/:keyId')
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('projectInternalId', [$project->getSequence()]),
Query::or([
Query::equal('projectInternalId', [$project->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
])
])
]);
if ($key->isEmpty()) {
@@ -1631,7 +1649,13 @@ App::put('/v1/projects/:projectId/keys/:keyId')
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('projectInternalId', [$project->getSequence()]),
Query::or([
Query::equal('projectInternalId', [$project->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
])
])
]);
if ($key->isEmpty()) {
@@ -1682,7 +1706,13 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('projectInternalId', [$project->getSequence()]),
Query::or([
Query::equal('projectInternalId', [$project->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
])
])
]);
if ($key->isEmpty()) {
+8 -4
View File
@@ -1464,7 +1464,8 @@ App::get('/v1/teams/:teamId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $teamId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $teamId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$team = $dbForProject->getDocument('teams', $teamId);
@@ -1478,9 +1479,12 @@ App::get('/v1/teams/:teamId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$audit = new Audit($dbForProject);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$resource = 'team/' . $team->getId();
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, offset: $offset, limit: $limit);
$output = [];
@@ -1527,7 +1531,7 @@ App::get('/v1/teams/:teamId/logs')
}
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
+8 -4
View File
@@ -945,7 +945,8 @@ App::get('/v1/users/:userId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $userId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $userId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$user = $dbForProject->getDocument('users', $userId);
@@ -958,8 +959,11 @@ App::get('/v1/users/:userId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$audit = new Audit($dbForProject);
$logs = $audit->getLogsByUser($user->getSequence(), $queries);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$logs = $audit->getLogsByUser($user->getSequence(), limit: $limit, offset: $offset);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
@@ -999,7 +1003,7 @@ App::get('/v1/users/:userId/logs')
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByUser($user->getSequence(), $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByUser($user->getSequence()) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
-43
View File
@@ -1,43 +0,0 @@
<?php
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
App::init()
->groups(['web'])
->inject('request')
->inject('response')
->action(function (Request $request, Response $response) {
$response
->addHeader('X-Frame-Options', 'SAMEORIGIN') // Avoid console and homepage from showing in iframes
->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url=' . \urlencode($request->getURI()))
->addHeader('X-UA-Compatible', 'IE=Edge') // Deny IE browsers from going into quirks mode
;
});
App::get('/')
->alias('auth/*')
->alias('/invite')
->alias('/login')
->alias('/mfa')
->alias('/card/*')
->alias('/recover')
->alias('/register/*')
->groups(['web'])
->label('permission', 'public')
->label('scope', 'home')
->inject('request')
->inject('response')
->action(function (Request $request, Response $response) {
$url = parse_url($request->getURI());
$target = "/console{$url['path']}";
$params = $request->getParams();
if (!empty($params)) {
$target .= "?" . \http_build_query($params);
}
if ($url['fragment'] ?? false) {
$target .= "#{$url['fragment']}";
}
$response->redirect($target);
});
+8 -4
View File
@@ -12,6 +12,8 @@ use Swoole\Process;
use Swoole\Table;
use Swoole\Timer;
use Utopia\App;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Adapter\SQL as AuditAdapterSQL;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Compression\Compression;
@@ -260,8 +262,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
// create appwrite database, `dbForPlatform` is a direct access call.
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections) {
if ($dbForPlatform->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForPlatform);
if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForPlatform);
$audit = new Audit($adapter);
$audit->setup();
}
@@ -389,8 +392,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
Console::success('[Setup] - Skip: metadata table already exists');
}
if ($dbForProject->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForProject);
if ($dbForProject->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForProject);
$audit = new Audit($adapter);
$audit->setup();
}
+7 -1
View File
@@ -136,7 +136,13 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('keys', [
Query::equal('projectInternalId', [$document->getSequence()]),
Query::or([
Query::equal('projectInternalId', [$document->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$document->getSequence()]),
])
]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
}
+9 -2
View File
@@ -4,7 +4,7 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Audit;
use Appwrite\Event\Audit as AuditEvent;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
@@ -30,6 +30,8 @@ use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit;
use Utopia\Auth\Hashes\Argon2;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Code;
@@ -146,7 +148,7 @@ App::setResource('queueForStatsUsage', function (Publisher $publisher) {
return new StatsUsage($publisher);
}, ['publisher']);
App::setResource('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
return new AuditEvent($publisher);
}, ['publisher']);
App::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
@@ -642,6 +644,11 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
};
}, ['pools', 'cache']);
App::setResource('audit', function ($dbForProject) {
$adapter = new AdapterDatabase($dbForProject);
return new Audit($adapter);
}, ['dbForProject']);
App::setResource('telemetry', fn () => new NoTelemetry());
App::setResource('cache', function (Group $pools, Telemetry $telemetry) {
+3
View File
@@ -11,6 +11,7 @@ $httpsPort = $this->getParam('httpsPort', '');
$version = $this->getParam('version', '');
$organization = $this->getParam('organization', '');
$image = $this->getParam('image', '');
$enableAssistant = $this->getParam('enableAssistant', false);
?>services:
traefik:
image: traefik:2.11
@@ -848,6 +849,7 @@ $image = $this->getParam('image', '');
- _APP_DB_USER
- _APP_DB_PASS
<?php if ($enableAssistant): ?>
appwrite-assistant:
image: appwrite/assistant:0.8.4
container_name: appwrite-assistant
@@ -857,6 +859,7 @@ $image = $this->getParam('image', '');
- appwrite
environment:
- _APP_ASSISTANT_OPENAI_API_KEY
<?php endif; ?>
appwrite-browser:
image: appwrite/browser:0.3.2
+15
View File
@@ -21,6 +21,8 @@ use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit as UtopiaAudit;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@@ -450,6 +452,19 @@ Server::setResource('logError', function (Registry $register, Document $project)
Server::setResource('executor', fn () => new Executor());
Server::setResource('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
if ($project->isEmpty() || $project->getId() === 'console') {
$adapter = new AdapterDatabase($dbForPlatform);
return new UtopiaAudit($adapter);
}
$dbForProject = $getProjectDB($project);
$adapter = new AdapterDatabase($dbForProject);
return new UtopiaAudit($adapter);
};
}, ['dbForPlatform', 'getProjectDB']);
$pools = $register->get('pools');
$platform = new Appwrite();
$args = $platform->getEnv('argv');
+3 -3
View File
@@ -47,12 +47,12 @@
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "1.*",
"utopia-php/audit": "2.0.2-rc1",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "1.*.*",
"utopia-php/database": "3.*",
"utopia-php/database": "3.*.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "0.9.*",
"utopia-php/emails": "0.6.*",
@@ -109,4 +109,4 @@
"tbachert/spi": true
}
}
}
}
Generated
+79 -75
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7c9cb03eb5267f1e7a3ffc037ae22b6a",
"content-hash": "b873febd2b03c32ec61a57b690cc44a2",
"packages": [
{
"name": "adhocore/jwt",
@@ -2673,16 +2673,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.4.1",
"version": "v7.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "26cc224ea7103dda90e9694d9e139a389092d007"
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/26cc224ea7103dda90e9694d9e139a389092d007",
"reference": "26cc224ea7103dda90e9694d9e139a389092d007",
"url": "https://api.github.com/repos/symfony/http-client/zipball/d01dfac1e0dc99f18da48b18101c23ce57929616",
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616",
"shasum": ""
},
"require": {
@@ -2750,7 +2750,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.4.1"
"source": "https://github.com/symfony/http-client/tree/v7.4.3"
},
"funding": [
{
@@ -2770,7 +2770,7 @@
"type": "tidelift"
}
],
"time": "2025-12-04T21:12:57+00:00"
"time": "2025-12-23T14:50:43+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -3552,21 +3552,23 @@
},
{
"name": "utopia-php/audit",
"version": "1.0.2",
"version": "2.0.2-rc1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "8c17065c2473d4ca799f65585ca74eb53e1be211"
"reference": "7b35dab40bce66bda56eeeacd2bbcbf1e823f05f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/8c17065c2473d4ca799f65585ca74eb53e1be211",
"reference": "8c17065c2473d4ca799f65585ca74eb53e1be211",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/7b35dab40bce66bda56eeeacd2bbcbf1e823f05f",
"reference": "7b35dab40bce66bda56eeeacd2bbcbf1e823f05f",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/database": "*"
"utopia-php/database": "3.*",
"utopia-php/fetch": "^0.4.2",
"utopia-php/validators": "^0.1.0"
},
"require-dev": {
"laravel/pint": "1.*",
@@ -3593,9 +3595,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/1.0.2"
"source": "https://github.com/utopia-php/audit/tree/2.0.2-rc1"
},
"time": "2025-10-20T07:14:26+00:00"
"time": "2025-12-24T01:20:43+00:00"
},
{
"name": "utopia-php/auth",
@@ -3654,16 +3656,16 @@
},
{
"name": "utopia-php/cache",
"version": "0.13.1",
"version": "0.13.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cache.git",
"reference": "97220cb3b3822b166ee016d1646e2ae2815dc540"
"reference": "5768498c9f451482f0bf3eede4d6452ddcd4a0f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/cache/zipball/97220cb3b3822b166ee016d1646e2ae2815dc540",
"reference": "97220cb3b3822b166ee016d1646e2ae2815dc540",
"url": "https://api.github.com/repos/utopia-php/cache/zipball/5768498c9f451482f0bf3eede4d6452ddcd4a0f6",
"reference": "5768498c9f451482f0bf3eede4d6452ddcd4a0f6",
"shasum": ""
},
"require": {
@@ -3672,7 +3674,7 @@
"ext-redis": "*",
"php": ">=8.0",
"utopia-php/pools": "0.8.*",
"utopia-php/telemetry": "0.1.*"
"utopia-php/telemetry": "*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@@ -3700,9 +3702,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cache/issues",
"source": "https://github.com/utopia-php/cache/tree/0.13.1"
"source": "https://github.com/utopia-php/cache/tree/0.13.2"
},
"time": "2025-05-09T14:43:52+00:00"
"time": "2025-12-17T08:55:43+00:00"
},
{
"name": "utopia-php/cli",
@@ -3896,16 +3898,16 @@
},
{
"name": "utopia-php/database",
"version": "3.6.0",
"version": "3.6.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "af15066255a5fd7bd2926de37bcbf3d8500fc155"
"reference": "c8c1b2f5770245dd4006e2680681e3efbe8b1fa7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/af15066255a5fd7bd2926de37bcbf3d8500fc155",
"reference": "af15066255a5fd7bd2926de37bcbf3d8500fc155",
"url": "https://api.github.com/repos/utopia-php/database/zipball/c8c1b2f5770245dd4006e2680681e3efbe8b1fa7",
"reference": "c8c1b2f5770245dd4006e2680681e3efbe8b1fa7",
"shasum": ""
},
"require": {
@@ -3948,9 +3950,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/3.6.0"
"source": "https://github.com/utopia-php/database/tree/3.6.1"
},
"time": "2025-12-08T05:23:04+00:00"
"time": "2025-12-16T09:55:41+00:00"
},
{
"name": "utopia-php/detector",
@@ -3999,23 +4001,23 @@
},
{
"name": "utopia-php/dns",
"version": "1.4.0",
"version": "1.4.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/dns.git",
"reference": "dce3453364a4524b7250db8d8eb74820b814409e"
"reference": "5daf8b683dad877491c4df84c6be24850b2f363b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/dns/zipball/dce3453364a4524b7250db8d8eb74820b814409e",
"reference": "dce3453364a4524b7250db8d8eb74820b814409e",
"url": "https://api.github.com/repos/utopia-php/dns/zipball/5daf8b683dad877491c4df84c6be24850b2f363b",
"reference": "5daf8b683dad877491c4df84c6be24850b2f363b",
"shasum": ""
},
"require": {
"php": ">=8.3",
"utopia-php/console": "0.0.*",
"utopia-php/domains": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/telemetry": "*",
"utopia-php/validators": "0.*"
},
"require-dev": {
@@ -4050,9 +4052,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/dns/issues",
"source": "https://github.com/utopia-php/dns/tree/1.4.0"
"source": "https://github.com/utopia-php/dns/tree/1.4.1"
},
"time": "2025-12-05T10:09:00+00:00"
"time": "2025-12-17T09:09:08+00:00"
},
{
"name": "utopia-php/domains",
@@ -4730,21 +4732,21 @@
},
{
"name": "utopia-php/pools",
"version": "0.8.2",
"version": "0.8.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/pools.git",
"reference": "05c67aba42eb68ac65489cc1e7fc5db83db2dd4d"
"reference": "ad7d6ba946376e81c603204285ce9a674b6502b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/pools/zipball/05c67aba42eb68ac65489cc1e7fc5db83db2dd4d",
"reference": "05c67aba42eb68ac65489cc1e7fc5db83db2dd4d",
"url": "https://api.github.com/repos/utopia-php/pools/zipball/ad7d6ba946376e81c603204285ce9a674b6502b8",
"reference": "ad7d6ba946376e81c603204285ce9a674b6502b8",
"shasum": ""
},
"require": {
"php": ">=8.3",
"utopia-php/telemetry": "0.1.*"
"php": ">=8.4",
"utopia-php/telemetry": "*"
},
"require-dev": {
"laravel/pint": "1.*",
@@ -4776,9 +4778,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/pools/issues",
"source": "https://github.com/utopia-php/pools/tree/0.8.2"
"source": "https://github.com/utopia-php/pools/tree/0.8.3"
},
"time": "2025-04-17T02:04:54+00:00"
"time": "2025-12-17T09:35:18+00:00"
},
{
"name": "utopia-php/preloader",
@@ -4835,16 +4837,16 @@
},
{
"name": "utopia-php/queue",
"version": "0.11.1",
"version": "0.11.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/queue.git",
"reference": "498bbbef418b1db71b51e1bb62f5d1d752ddd8d6"
"reference": "a854f7c4abc18e0eca55fc5608cd7088d71eb19f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/498bbbef418b1db71b51e1bb62f5d1d752ddd8d6",
"reference": "498bbbef418b1db71b51e1bb62f5d1d752ddd8d6",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/a854f7c4abc18e0eca55fc5608cd7088d71eb19f",
"reference": "a854f7c4abc18e0eca55fc5608cd7088d71eb19f",
"shasum": ""
},
"require": {
@@ -4854,7 +4856,7 @@
"utopia-php/fetch": "0.4.*",
"utopia-php/framework": "0.33.*",
"utopia-php/pools": "0.8.*",
"utopia-php/telemetry": "0.1.*"
"utopia-php/telemetry": "*"
},
"require-dev": {
"ext-redis": "*",
@@ -4895,9 +4897,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/queue/issues",
"source": "https://github.com/utopia-php/queue/tree/0.11.1"
"source": "https://github.com/utopia-php/queue/tree/0.11.2"
},
"time": "2025-05-30T11:50:34+00:00"
"time": "2025-12-17T09:32:35+00:00"
},
{
"name": "utopia-php/registry",
@@ -4953,16 +4955,16 @@
},
{
"name": "utopia-php/storage",
"version": "0.18.16",
"version": "0.18.18",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/storage.git",
"reference": "0c7b8ad68de8e1eb23ccc8af9f27a30eb832930f"
"reference": "acaea524f315f87b8811a2c34450fe2b502f49d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/storage/zipball/0c7b8ad68de8e1eb23ccc8af9f27a30eb832930f",
"reference": "0c7b8ad68de8e1eb23ccc8af9f27a30eb832930f",
"url": "https://api.github.com/repos/utopia-php/storage/zipball/acaea524f315f87b8811a2c34450fe2b502f49d8",
"reference": "acaea524f315f87b8811a2c34450fe2b502f49d8",
"shasum": ""
},
"require": {
@@ -5005,9 +5007,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/storage/issues",
"source": "https://github.com/utopia-php/storage/tree/0.18.16"
"source": "https://github.com/utopia-php/storage/tree/0.18.18"
},
"time": "2025-12-03T02:15:45+00:00"
"time": "2025-12-17T07:33:45+00:00"
},
{
"name": "utopia-php/swoole",
@@ -5436,16 +5438,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.5.9",
"version": "1.8.6",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "ee434aa00a9185380b9a39bb46bf86d7104d3a93"
"reference": "b6cc29d3bd247e193f3c06b4168dc69d884645f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/ee434aa00a9185380b9a39bb46bf86d7104d3a93",
"reference": "ee434aa00a9185380b9a39bb46bf86d7104d3a93",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/b6cc29d3bd247e193f3c06b4168dc69d884645f0",
"reference": "b6cc29d3bd247e193f3c06b4168dc69d884645f0",
"shasum": ""
},
"require": {
@@ -5481,9 +5483,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.5.9"
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.6"
},
"time": "2025-11-25T05:22:25+00:00"
"time": "2025-12-31T10:22:17+00:00"
},
{
"name": "doctrine/annotations",
@@ -7931,16 +7933,16 @@
},
{
"name": "symfony/console",
"version": "v8.0.1",
"version": "v8.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "fcb73f69d655b48fcb894a262f074218df08bd58"
"reference": "6145b304a5c1ea0bdbd0b04d297a5864f9a7d587"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/fcb73f69d655b48fcb894a262f074218df08bd58",
"reference": "fcb73f69d655b48fcb894a262f074218df08bd58",
"url": "https://api.github.com/repos/symfony/console/zipball/6145b304a5c1ea0bdbd0b04d297a5864f9a7d587",
"reference": "6145b304a5c1ea0bdbd0b04d297a5864f9a7d587",
"shasum": ""
},
"require": {
@@ -7997,7 +7999,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v8.0.1"
"source": "https://github.com/symfony/console/tree/v8.0.3"
},
"funding": [
{
@@ -8017,7 +8019,7 @@
"type": "tidelift"
}
],
"time": "2025-12-05T15:25:33+00:00"
"time": "2025-12-23T14:52:06+00:00"
},
{
"name": "symfony/filesystem",
@@ -8091,16 +8093,16 @@
},
{
"name": "symfony/finder",
"version": "v8.0.0",
"version": "v8.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291"
"reference": "dd3a2953570a283a2ba4e17063bb98c734cf5b12"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/7598dd5770580fa3517ec83e8da0c9b9e01f4291",
"reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291",
"url": "https://api.github.com/repos/symfony/finder/zipball/dd3a2953570a283a2ba4e17063bb98c734cf5b12",
"reference": "dd3a2953570a283a2ba4e17063bb98c734cf5b12",
"shasum": ""
},
"require": {
@@ -8135,7 +8137,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v8.0.0"
"source": "https://github.com/symfony/finder/tree/v8.0.3"
},
"funding": [
{
@@ -8155,7 +8157,7 @@
"type": "tidelift"
}
],
"time": "2025-11-05T14:36:47+00:00"
"time": "2025-12-23T14:52:06+00:00"
},
{
"name": "symfony/options-resolver",
@@ -8943,7 +8945,9 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"utopia-php/audit": 5
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -8967,5 +8971,5 @@
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}
@@ -0,0 +1,92 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Assistant;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName(): string
{
return 'createAssistantQuery';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/console/assistant')
->desc('Create assistant query')
->groups(['api', 'assistant'])
->label('scope', 'assistant.read')
->label('sdk', new Method(
namespace: 'assistant',
group: 'console',
name: 'chat',
description: '/docs/references/assistant/chat.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::TEXT
))
->label('abuse-limit', 15)
->label('abuse-key', 'userId:{userId}')
->param('prompt', '', new Text(2000), 'Prompt. A string containing questions asked to the AI assistant.')
->inject('response')
->callback($this->action(...));
}
public function action(string $prompt, Response $response)
{
$ch = curl_init('http://appwrite-assistant:3003/v1/models/assistant/prompt');
$responseHeaders = [];
$query = json_encode(['prompt' => $prompt]);
$headers = ['accept: text/event-stream'];
$handleEvent = function ($ch, $data) use ($response) {
$response->chunk($data);
return \strlen($data);
};
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $handleEvent);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 9000);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
$len = strlen($header);
$header = explode(':', $header, 2);
if (count($header) < 2) { // ignore invalid headers
return $len;
}
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
return $len;
});
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
curl_exec($ch);
curl_close($ch);
$response->chunk('', true);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Init;
use Appwrite\Extend\Exception;
use Utopia\Database\Document;
use Utopia\Platform\Action;
class API extends Action
{
public static function getName(): string
{
return 'consoleAPI';
}
public function __construct()
{
$this
->setType(Action::TYPE_INIT)
->groups(['console'])
->inject('project')
->callback(function (Document $project) {
if ($project->getId() !== 'console') {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN);
}
});
}
}
@@ -0,0 +1,31 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Init;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Platform\Action;
class Web extends Action
{
public static function getName(): string
{
return 'consoleWeb';
}
public function __construct()
{
$this
->setType(Action::TYPE_INIT)
->groups(['web'])
->inject('request')
->inject('response')
->callback(function (Request $request, Response $response) {
$response
->addHeader('X-Frame-Options', 'SAMEORIGIN') // Avoid console and homepage from showing in iframes
->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url=' . \urlencode($request->getURI()))
->addHeader('X-UA-Compatible', 'IE=Edge') // Deny IE browsers from going into quirks mode
;
});
}
}
@@ -0,0 +1,18 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Redirects\Auth;
use Appwrite\Platform\Modules\Console\Http\Redirects\Base;
class Get extends Base
{
public static function getName(): string
{
return 'consoleRedirectAuth';
}
protected function getPath(): string
{
return '/auth/*';
}
}
@@ -0,0 +1,51 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Redirects;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
abstract class Base extends Action
{
use HTTP;
/**
* HTTP platform trait doesn't support multiple `aliases`
* like legacy controllers so we use independent redirects!
*
* This helps as a base and a small code logic for maintenance.
*
* @return string
*/
abstract protected function getPath(): string;
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath($this->getPath())
->groups(['web'])
->label('permission', 'public')
->label('scope', 'home')
->inject('request')
->inject('response')
->callback($this->action(...));
}
public function action(Request $request, Response $response): void
{
$url = parse_url($request->getURI());
$target = "/console{$url['path']}";
$params = $request->getParams();
if (!empty($params)) {
$target .= "?" . \http_build_query($params);
}
if ($url['fragment'] ?? false) {
$target .= "#{$url['fragment']}";
}
$response->redirect($target);
}
}
@@ -0,0 +1,18 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Redirects\Card;
use Appwrite\Platform\Modules\Console\Http\Redirects\Base;
class Get extends Base
{
public static function getName(): string
{
return 'consoleRedirectCard';
}
protected function getPath(): string
{
return '/card/*';
}
}
@@ -0,0 +1,18 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Redirects\Invite;
use Appwrite\Platform\Modules\Console\Http\Redirects\Base;
class Get extends Base
{
public static function getName(): string
{
return 'consoleRedirectInvite';
}
protected function getPath(): string
{
return '/invite';
}
}
@@ -0,0 +1,18 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Redirects\Login;
use Appwrite\Platform\Modules\Console\Http\Redirects\Base;
class Get extends Base
{
public static function getName(): string
{
return 'consoleRedirectLogin';
}
protected function getPath(): string
{
return '/login';
}
}
@@ -0,0 +1,18 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Redirects\MFA;
use Appwrite\Platform\Modules\Console\Http\Redirects\Base;
class Get extends Base
{
public static function getName(): string
{
return 'consoleRedirectMFA';
}
protected function getPath(): string
{
return '/mfa';
}
}
@@ -0,0 +1,18 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Redirects\Recover;
use Appwrite\Platform\Modules\Console\Http\Redirects\Base;
class Get extends Base
{
public static function getName(): string
{
return 'consoleRedirectRecover';
}
protected function getPath(): string
{
return '/recover';
}
}
@@ -0,0 +1,18 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Redirects\Register;
use Appwrite\Platform\Modules\Console\Http\Redirects\Base;
class Get extends Base
{
public static function getName(): string
{
return 'consoleRedirectRegister';
}
protected function getPath(): string
{
return '/register/*';
}
}
@@ -0,0 +1,18 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Redirects\Root;
use Appwrite\Platform\Modules\Console\Http\Redirects\Base;
class Get extends Base
{
public static function getName(): string
{
return 'consoleRedirectRoot';
}
protected function getPath(): string
{
return '/';
}
}
@@ -0,0 +1,93 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Variables;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\IP;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getVariables';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/console/variables')
->desc('Get variables')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'console',
group: 'console',
name: 'variables',
description: '/docs/references/console/variables.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_CONSOLE_VARIABLES,
)
],
contentType: ContentType::JSON
))
->inject('response')
->callback($this->action(...));
}
public function action(Response $response)
{
$validator = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME'));
$isCNAMEValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')) && $validator->isKnown() && !$validator->isTest();
$validator = new IP(IP::V4);
$isAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_A', '')) && ($validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_A')));
$validator = new IP(IP::V6);
$isAAAAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_AAAA', '')) && $validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA'));
$isDomainEnabled = $isAAAAValid || $isAValid || $isCNAMEValid;
$isVcsEnabled = !empty(System::getEnv('_APP_VCS_GITHUB_APP_NAME', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_APP_ID', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''));
$isAssistantEnabled = !empty(System::getEnv('_APP_ASSISTANT_OPENAI_API_KEY', ''));
$variables = new Document([
'_APP_DOMAIN_TARGET_CNAME' => System::getEnv('_APP_DOMAIN_TARGET_CNAME'),
'_APP_DOMAIN_TARGET_AAAA' => System::getEnv('_APP_DOMAIN_TARGET_AAAA'),
'_APP_DOMAIN_TARGET_A' => System::getEnv('_APP_DOMAIN_TARGET_A'),
'_APP_DOMAIN_TARGET_CAA' => '0 issue "' . System::getEnv('_APP_DOMAIN_TARGET_CAA') . '"',
'_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'),
'_APP_COMPUTE_BUILD_TIMEOUT' => +System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT'),
'_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'),
'_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'),
'_APP_VCS_ENABLED' => $isVcsEnabled,
'_APP_DOMAIN_ENABLED' => $isDomainEnabled,
'_APP_ASSISTANT_ENABLED' => $isAssistantEnabled,
'_APP_DOMAIN_SITES' => System::getEnv('_APP_DOMAIN_SITES'),
'_APP_DOMAIN_FUNCTIONS' => System::getEnv('_APP_DOMAIN_FUNCTIONS'),
'_APP_OPTIONS_FORCE_HTTPS' => System::getEnv('_APP_OPTIONS_FORCE_HTTPS'),
'_APP_DOMAINS_NAMESERVERS' => System::getEnv('_APP_DOMAINS_NAMESERVERS'),
]);
$response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES);
}
}
@@ -2,7 +2,19 @@
namespace Appwrite\Platform\Modules\Console\Services;
use Appwrite\Platform\Modules\Console\Http\Assistant\Create as CreateAssistantQuery;
use Appwrite\Platform\Modules\Console\Http\Init\API;
use Appwrite\Platform\Modules\Console\Http\Init\Web;
use Appwrite\Platform\Modules\Console\Http\Redirects\Auth\Get as RedirectAuth;
use Appwrite\Platform\Modules\Console\Http\Redirects\Card\Get as RedirectCard;
use Appwrite\Platform\Modules\Console\Http\Redirects\Invite\Get as RedirectInvite;
use Appwrite\Platform\Modules\Console\Http\Redirects\Login\Get as RedirectLogin;
use Appwrite\Platform\Modules\Console\Http\Redirects\MFA\Get as RedirectMFA;
use Appwrite\Platform\Modules\Console\Http\Redirects\Recover\Get as RedirectRecover;
use Appwrite\Platform\Modules\Console\Http\Redirects\Register\Get as RedirectRegister;
use Appwrite\Platform\Modules\Console\Http\Redirects\Root\Get as RedirectRoot;
use Appwrite\Platform\Modules\Console\Http\Resources\Get as GetResourceAvailability;
use Appwrite\Platform\Modules\Console\Http\Variables\Get as GetVariables;
use Utopia\Platform\Service;
class Http extends Service
@@ -10,7 +22,23 @@ class Http extends Service
public function __construct()
{
$this->type = Service::TYPE_HTTP;
// Resources
// API and Web init hooks!
$this->addAction(API::getName(), new API());
$this->addAction(Web::getName(), new Web());
$this->addAction(GetVariables::getName(), new GetVariables());
$this->addAction(CreateAssistantQuery::getName(), new CreateAssistantQuery());
$this->addAction(GetResourceAvailability::getName(), new GetResourceAvailability());
// web redirects to /console
$this->addAction(RedirectRoot::getName(), new RedirectRoot());
$this->addAction(RedirectAuth::getName(), new RedirectAuth());
$this->addAction(RedirectInvite::getName(), new RedirectInvite());
$this->addAction(RedirectLogin::getName(), new RedirectLogin());
$this->addAction(RedirectMFA::getName(), new RedirectMFA());
$this->addAction(RedirectCard::getName(), new RedirectCard());
$this->addAction(RedirectRecover::getName(), new RedirectRecover());
$this->addAction(RedirectRegister::getName(), new RedirectRegister());
}
}
@@ -72,10 +72,11 @@ class XList extends Action
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('audit')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb): void
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
@@ -98,12 +99,16 @@ class XList extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$audit = new Audit($dbForProject);
$type = $this->getCollectionsEventsContext();
$context = $this->getContext();
$resource = "database/$databaseId/$type/$collectionId/$context/{$document->getId()}";
$logs = $audit->getLogsByResource($resource, $queries);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$logs = $audit->getLogsByResource($resource, limit: $limit, offset: $offset);
$output = [];
@@ -152,7 +157,7 @@ class XList extends Action
$response->dynamic(new Document([
'logs' => $output,
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
]), $this->getResponseModel());
}
}
@@ -71,10 +71,11 @@ class XList extends Action
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('audit')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb): void
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -95,10 +96,13 @@ class XList extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$audit = new Audit($dbForProject);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$context = $this->getContext();
$resource = "database/$databaseId/$context/$collectionId";
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, limit: $limit, offset: $offset);
$output = [];
@@ -147,7 +151,7 @@ class XList extends Action
$response->dynamic(new Document([
'logs' => $output,
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
]), $this->getResponseModel());
}
}
@@ -67,10 +67,11 @@ class XList extends Action
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('audit')
->callback($this->action(...));
}
public function action(string $databaseId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb): void
public function action(string $databaseId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
@@ -84,9 +85,13 @@ class XList extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$audit = new Audit($dbForProject);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$resource = 'database/' . $databaseId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, limit: $limit, offset: $offset);
$output = [];
@@ -133,7 +138,7 @@ class XList extends Action
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
]), UtopiaResponse::MODEL_LOG_LIST);
}
@@ -62,10 +62,11 @@ class XList extends Action
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('audit')
->callback($this->action(...));
}
public function action(string $databaseId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb): void
public function action(string $databaseId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
@@ -79,9 +80,12 @@ class XList extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$audit = new Audit($dbForProject);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$resource = 'database/' . $databaseId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, limit: $limit, offset: $offset);
$output = [];
@@ -128,7 +132,7 @@ class XList extends Action
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
]), UtopiaResponse::MODEL_LOG_LIST);
}
@@ -50,6 +50,7 @@ class XList extends CollectionLogXList
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('audit')
->callback($this->action(...));
}
}
@@ -51,6 +51,7 @@ class XList extends DocumentLogXList
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('audit')
->callback($this->action(...));
}
}
+54 -1
View File
@@ -148,11 +148,63 @@ class Install extends Action
$httpsPort = ($httpsPort) ? $httpsPort : $defaultHTTPSPort;
}
$enableAssistant = false;
$assistantExistsInOldCompose = false;
if ($data !== false && isset($compose)) {
try {
$assistantService = $compose->getService('appwrite-assistant');
$assistantExistsInOldCompose = $assistantService !== null;
} catch (\Throwable) {
// assistant service doesn't exist, keep default false
}
}
if ($interactive == 'Y' && Console::isInteractive()) {
$prompt = 'Add Appwrite Assistant? (Y/n)' . ($assistantExistsInOldCompose ? ' [Currently enabled]' : '');
$answer = Console::confirm($prompt);
if (empty($answer)) {
$enableAssistant = $assistantExistsInOldCompose;
} else {
$enableAssistant = \strtolower($answer) === 'y';
}
} elseif ($assistantExistsInOldCompose) {
$enableAssistant = true;
}
$input = [];
$password = new Password();
$token = new Token();
foreach ($vars as $var) {
if ($var['name'] === '_APP_ASSISTANT_OPENAI_API_KEY') {
if (!$enableAssistant) {
$input[$var['name']] = '';
continue;
}
// key already exists
if (!empty($var['default'])) {
$input[$var['name']] = $var['default'];
continue;
}
// if assistant enabled and no key, ask for it
if (Console::isInteractive() && $interactive === 'Y') {
$input[$var['name']] = Console::confirm('Enter your OpenAI API key for Appwrite Assistant:');
if (empty($input[$var['name']])) {
Console::warning('No API key provided. Assistant will be disabled.');
$enableAssistant = false;
$input[$var['name']] = '';
}
continue;
}
$input[$var['name']] = '';
continue;
}
if (!empty($var['filter']) && ($interactive !== 'Y' || !Console::isInteractive())) {
if ($data && $var['default'] !== null) {
$input[$var['name']] = $var['default'];
@@ -199,7 +251,8 @@ class Install extends Action
->setParam('httpsPort', $httpsPort)
->setParam('version', APP_VERSION_STABLE)
->setParam('organization', $organization)
->setParam('image', $image);
->setParam('image', $image)
->setParam('enableAssistant', $enableAssistant);
$templateForEnv->setParam('vars', $input);
+2 -7
View File
@@ -75,15 +75,10 @@ class SDKs extends Action
$git = ($git === 'yes');
$prUrls = [];
$createPr = false;
if ($git) {
$production ??= Console::confirm('Type "Appwrite" to push code to production git repos');
$production = $production === 'Appwrite';
$production = ($production === 'yes');
$message ??= Console::confirm('Please enter your commit message:');
$createPr = Console::confirm('Should we create pull request automatically? (yes/no)');
$createPr = ($createPr === 'yes');
}
}
@@ -452,7 +447,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
');
Console::success("Pushed {$language['name']} SDK to {$gitUrl}");
if ($createPr) {
if ($git) {
$prTitle = "feat: {$language['name']} SDK update for version {$language['version']}";
$prBody = "This PR contains updates to the {$language['name']} SDK for version {$language['version']}.";
+5 -6
View File
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Workers;
use Exception;
use Throwable;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization;
@@ -42,8 +41,8 @@ class Audits extends Action
$this
->desc('Audits worker')
->inject('message')
->inject('getProjectDB')
->inject('project')
->inject('getAudit')
->callback($this->action(...));
$this->lastTriggeredTime = time();
@@ -54,13 +53,14 @@ class Audits extends Action
* @param Message $message
* @param callable $getProjectDB
* @param Document $project
* @param callable $getAudit
* @return Commit|NoCommit
* @throws Throwable
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Structure
*/
public function action(Message $message, callable $getProjectDB, Document $project): Commit|NoCommit
public function action(Message $message, Document $project, callable $getAudit): Commit|NoCommit
{
$payload = $message->getPayload() ?? [];
@@ -102,7 +102,7 @@ class Audits extends Action
'mode' => $mode,
'data' => $auditPayload,
],
'timestamp' => date("Y-m-d H:i:s", $message->getTimestamp()),
'time' => date("Y-m-d H:i:s", $message->getTimestamp()),
];
if (isset($this->logs[$project->getSequence()])) {
@@ -135,8 +135,7 @@ class Audits extends Action
Console::log('Processing Project "' . $sequence . '" batch with ' . count($projectLogs['logs']) . ' events');
$projectDocument = $projectLogs['project'];
$dbForProject = $getProjectDB($projectDocument);
$audit = new Audit($dbForProject);
$audit = $getAudit($projectDocument);
$audit->logBatch($projectLogs['logs']);
Console::success('Audit logs processed successfully');
+20 -14
View File
@@ -9,6 +9,7 @@ use Appwrite\Extend\Exception;
use Executor\Executor;
use Throwable;
use Utopia\Abuse\Adapters\TimeLimit\Database as AbuseDatabase;
use Utopia\Audit\Adapter\SQL;
use Utopia\Audit\Audit;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
@@ -62,6 +63,7 @@ class Deletes extends Action
->inject('executionRetention')
->inject('auditRetention')
->inject('log')
->inject('getAudit')
->callback($this->action(...));
}
@@ -84,7 +86,8 @@ class Deletes extends Action
Executor $executor,
string $executionRetention,
string $auditRetention,
Log $log
Log $log,
callable $getAudit,
): void {
$payload = $message->getPayload() ?? [];
@@ -145,7 +148,7 @@ class Deletes extends Action
break;
case DELETE_TYPE_AUDIT:
if (!$project->isEmpty()) {
$this->deleteAuditLogs($project, $getProjectDB, $auditRetention);
$this->deleteAuditLogs($project, $auditRetention, $getAudit);
}
break;
case DELETE_TYPE_REALTIME:
@@ -517,7 +520,7 @@ class Deletes extends Action
$projectCollectionIds = [
...\array_keys(Config::getParam('collections', [])['projects']),
Audit::COLLECTION,
SQL::COLLECTION,
AbuseDatabase::COLLECTION,
];
@@ -565,7 +568,13 @@ class Deletes extends Action
// Delete Keys
$this->deleteByGroup('keys', [
Query::equal('projectInternalId', [$projectInternalId]),
Query::or([
Query::equal('projectInternalId', [$projectInternalId]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$projectInternalId]),
])
]),
Query::orderAsc()
], $dbForPlatform);
@@ -777,23 +786,20 @@ class Deletes extends Action
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $auditRetention
* @param callable $getAudit
* @return void
* @throws Exception
*/
private function deleteAuditLogs(Document $project, callable $getProjectDB, string $auditRetention): void
private function deleteAuditLogs(Document $project, string $auditRetention, callable $getAudit): void
{
$projectId = $project->getId();
$dbForProject = $getProjectDB($project);
/** @var Audit $audit */
$audit = $getAudit($project);
try {
$this->deleteByGroup(Audit::COLLECTION, [
Query::select([...$this->selects, 'time']),
Query::lessThan('time', $auditRetention),
Query::orderDesc('time'),
Query::orderAsc(),
], $dbForProject);
} catch (DatabaseException $e) {
Console::error('Failed to delete audit logs for project ' . $projectId . ': ' . $e->getMessage());
$audit->cleanup(new \DateTime($auditRetention));
} catch (Throwable $th) {
Console::error('Failed to delete audit logs for project ' . $projectId . ': ' . $th->getMessage());
}
}
+5 -4
View File
@@ -200,7 +200,8 @@ class Migrations extends Action
default => throw new \Exception('Invalid source type'),
};
$this->sourceReport = $migrationSource->report();
$resources = $migration->getAttribute('resources', []);
$this->sourceReport = $migrationSource->report($resources);
return $migrationSource;
}
@@ -362,10 +363,10 @@ class Migrations extends Action
$migration->getAttribute('resourceId'),
$migration->getAttribute('resourceType')
);
}
$destination->shutdown();
$source->shutdown();
$destination->shutdown();
$source->shutdown();
}
$sourceErrors = $source->getErrors();
$destinationErrors = $destination->getErrors();
@@ -111,7 +111,13 @@ class StatsResources extends Action
Query::equal('projectInternalId', [$project->getSequence()])
]);
$keys = $dbForPlatform->count('keys', [
Query::equal('projectInternalId', [$project->getSequence()])
Query::or([
Query::equal('projectInternalId', [$project->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
])
]),
]);
$domains = $dbForPlatform->count('rules', [
@@ -1851,6 +1851,8 @@ class ProjectsConsoleClientTest extends Scope
$sessionCookie = $response['headers']['set-cookie'];
$sessionId2 = $response['body']['$id'];
sleep(5); // fixes flaky tests.
/**
* List sessions
*/