mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/1.9.x' into fix-large-file-download
This commit is contained in:
@@ -9,11 +9,5 @@ return [
|
||||
'mfaChallenge',
|
||||
'sessionAlert',
|
||||
'otpSession'
|
||||
],
|
||||
'sms' => [
|
||||
'verification',
|
||||
'login',
|
||||
'invitation',
|
||||
'mfaChallenge'
|
||||
]
|
||||
];
|
||||
|
||||
@@ -1487,13 +1487,13 @@ return [
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => 'crm-dashboard-react-admin',
|
||||
'name' => 'CRM dashboard with React Admin',
|
||||
'tagline' => 'A React-based admin dashboard template with CRM features.',
|
||||
'key' => 'dashboard-react-admin',
|
||||
'name' => 'E-commerce dashboard with React Admin',
|
||||
'tagline' => 'A React-based admin dashboard template with e-commerce features.',
|
||||
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
|
||||
'useCases' => [SiteUseCases::DASHBOARD],
|
||||
'screenshotDark' => $url . '/images/sites/templates/crm-dashboard-react-admin-dark.png',
|
||||
'screenshotLight' => $url . '/images/sites/templates/crm-dashboard-react-admin-light.png',
|
||||
'useCases' => [SiteUseCases::DASHBOARD, SiteUseCases::ECOMMERCE],
|
||||
'screenshotDark' => $url . '/images/sites/templates/dashboard-react-admin-dark.png',
|
||||
'screenshotLight' => $url . '/images/sites/templates/dashboard-react-admin-light.png',
|
||||
'frameworks' => [
|
||||
getFramework('REACT', [
|
||||
'providerRootDirectory' => './react/react-admin',
|
||||
|
||||
@@ -474,19 +474,26 @@ Http::delete('/v1/account')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForDeletes')
|
||||
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
|
||||
->inject('authorization')
|
||||
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes, Authorization $authorization) {
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
// get all memberships
|
||||
$memberships = $user->getAttribute('memberships', []);
|
||||
foreach ($memberships as $membership) {
|
||||
// prevent deletion if at least one active membership
|
||||
if ($membership->getAttribute('confirm', false)) {
|
||||
throw new Exception(Exception::USER_DELETION_PROHIBITED);
|
||||
if (!$membership->getAttribute('confirm', false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId'));
|
||||
if ($team->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Team is left as-is — we don't promote non-owner members to owner.
|
||||
// Orphan teams are cleaned up later by Cloud's inactive project cleanup.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2265,7 +2272,10 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
|
||||
$subject = $locale->getText("emails.magicSession.subject");
|
||||
$preview = $locale->getText("emails.magicSession.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ?? [];
|
||||
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.magicSession-' . $locale->fallback] ?? [];
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$agentOs = $detector->getOS();
|
||||
@@ -2575,7 +2585,9 @@ Http::post('/v1/account/tokens/email')
|
||||
$preview = $locale->getText("emails.otpSession.preview");
|
||||
$heading = $locale->getText("emails.otpSession.heading");
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.otpSession-' . $locale->fallback] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
@@ -2968,11 +2980,6 @@ Http::post('/v1/account/tokens/phone')
|
||||
if ($sendSMS) {
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['sms.login-' . $locale->default] ?? [];
|
||||
if (!empty($customTemplate)) {
|
||||
$message = $customTemplate['message'] ?? $message;
|
||||
}
|
||||
|
||||
$projectName = $project->getAttribute('name');
|
||||
if ($project->getId() === 'console') {
|
||||
$projectName = $platform['platformName'];
|
||||
@@ -3726,7 +3733,9 @@ Http::post('/v1/account/recovery')
|
||||
$body = $locale->getText("emails.recovery.body");
|
||||
$subject = $locale->getText("emails.recovery.subject");
|
||||
$preview = $locale->getText("emails.recovery.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.recovery-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.recovery-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.recovery-' . $locale->fallback] ?? [];
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
@@ -4034,7 +4043,9 @@ Http::post('/v1/account/verifications/email')
|
||||
$subject = $locale->getText("emails.verification.subject");
|
||||
$heading = $locale->getText("emails.verification.heading");
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.verification-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.verification-' . $locale->fallback] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
@@ -4333,11 +4344,6 @@ Http::post('/v1/account/verifications/phone')
|
||||
if ($sendSMS) {
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['sms.verification-' . $locale->default] ?? [];
|
||||
if (!empty($customTemplate)) {
|
||||
$message = $customTemplate['message'] ?? $message;
|
||||
}
|
||||
|
||||
$messageContent = Template::fromString($locale->getText("sms.verification.body"));
|
||||
$messageContent
|
||||
->setParam('{{project}}', $project->getAttribute('name'))
|
||||
|
||||
@@ -833,75 +833,8 @@ Http::post('/v1/projects/:projectId/smtp/tests')
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/templates/sms/:type/:locale')
|
||||
->desc('Get custom SMS template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'getSmsTemplate',
|
||||
description: '/docs/references/projects/get-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_TEMPLATE,
|
||||
)
|
||||
],
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'projects.getSMSTemplate',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'getSMSTemplate',
|
||||
description: '/docs/references/projects/get-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_TEMPLATE,
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? [], true), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
|
||||
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
|
||||
|
||||
if (is_null($template)) {
|
||||
$template = [
|
||||
'message' => Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl')->render(),
|
||||
];
|
||||
}
|
||||
|
||||
$template['type'] = $type;
|
||||
$template['locale'] = $locale;
|
||||
|
||||
$response->dynamic(new Document($template), Response::MODEL_SMS_TEMPLATE);
|
||||
});
|
||||
|
||||
|
||||
Http::get('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
Http::get('/v1/projects/:projectId/templates/email')
|
||||
->alias('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
->desc('Get custom email template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
@@ -920,10 +853,12 @@ Http::get('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
|
||||
->inject('locale')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform, Locale $localeObject) {
|
||||
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
@@ -1000,74 +935,8 @@ Http::get('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
$response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/templates/sms/:type/:locale')
|
||||
->desc('Update custom SMS template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'updateSmsTemplate',
|
||||
description: '/docs/references/projects/update-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_TEMPLATE,
|
||||
)
|
||||
],
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'projects.updateSMSTemplate',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'updateSMSTemplate',
|
||||
description: '/docs/references/projects/update-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_TEMPLATE,
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? [], true), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->param('message', '', new Text(0), 'Template message')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForPlatform) {
|
||||
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$templates['sms.' . $type . '-' . $locale] = [
|
||||
'message' => $message
|
||||
];
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'message' => $message,
|
||||
'type' => $type,
|
||||
'locale' => $locale,
|
||||
]), Response::MODEL_SMS_TEMPLATE);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
Http::patch('/v1/projects/:projectId/templates/email')
|
||||
->alias('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
->desc('Update custom email templates')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
@@ -1086,7 +955,7 @@ Http::patch('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
|
||||
->param('subject', '', new Text(255), 'Email Subject')
|
||||
->param('message', '', new Text(0), 'Template message')
|
||||
->param('senderName', '', new Text(255, 0), 'Name of the email sender', true)
|
||||
@@ -1094,7 +963,9 @@ Http::patch('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
->param('replyTo', '', new Email(), 'Reply to email', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForPlatform) {
|
||||
->inject('locale')
|
||||
->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForPlatform, Locale $localeObject) {
|
||||
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
@@ -1124,79 +995,8 @@ Http::patch('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
]), Response::MODEL_EMAIL_TEMPLATE);
|
||||
});
|
||||
|
||||
Http::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
|
||||
->desc('Reset custom SMS template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'deleteSmsTemplate',
|
||||
description: '/docs/references/projects/delete-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_TEMPLATE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON,
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'projects.deleteSMSTemplate',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'templates',
|
||||
name: 'deleteSMSTemplate',
|
||||
description: '/docs/references/projects/delete-sms-template.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_SMS_TEMPLATE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
)
|
||||
])
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? [], true), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
|
||||
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
|
||||
|
||||
if (is_null($template)) {
|
||||
throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION);
|
||||
}
|
||||
|
||||
unset($template['sms.' . $type . '-' . $locale]);
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'type' => $type,
|
||||
'locale' => $locale,
|
||||
'message' => $template['message']
|
||||
]), Response::MODEL_SMS_TEMPLATE);
|
||||
});
|
||||
|
||||
Http::delete('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
Http::delete('/v1/projects/:projectId/templates/email')
|
||||
->alias('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
->desc('Delete custom email template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
@@ -1216,10 +1016,12 @@ Http::delete('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', true, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) {
|
||||
->inject('locale')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform, Locale $localeObject) {
|
||||
$locale = $locale ?: $localeObject->default ?: $localeObject->fallback ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
|
||||
@@ -971,7 +971,8 @@ Http::shutdown()
|
||||
if ($useCache) {
|
||||
$resource = $resourceType = null;
|
||||
$data = $response->getPayload();
|
||||
if (! empty($data['payload'])) {
|
||||
$statusCode = $response->getStatusCode();
|
||||
if (! empty($data['payload']) && $statusCode >= 200 && $statusCode < 300) {
|
||||
$pattern = $route->getLabel('cache.resource', null);
|
||||
if (! empty($pattern)) {
|
||||
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
|
||||
+4
-4
@@ -117,7 +117,9 @@ use Appwrite\Utopia\Response\Model\Project;
|
||||
use Appwrite\Utopia\Response\Model\Provider;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepository;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
|
||||
use Appwrite\Utopia\Response\Model\ResourceToken;
|
||||
use Appwrite\Utopia\Response\Model\Row;
|
||||
use Appwrite\Utopia\Response\Model\Rule;
|
||||
@@ -135,7 +137,6 @@ use Appwrite\Utopia\Response\Model\TemplateFramework;
|
||||
use Appwrite\Utopia\Response\Model\TemplateFunction;
|
||||
use Appwrite\Utopia\Response\Model\TemplateRuntime;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSite;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSMS;
|
||||
use Appwrite\Utopia\Response\Model\TemplateVariable;
|
||||
use Appwrite\Utopia\Response\Model\Token;
|
||||
use Appwrite\Utopia\Response\Model\Topic;
|
||||
@@ -190,8 +191,8 @@ Response::setModel(new BaseList('Site Templates List', Response::MODEL_TEMPLATE_
|
||||
Response::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
|
||||
Response::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
|
||||
Response::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
|
||||
Response::setModel(new BaseList('Framework Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtime Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME));
|
||||
Response::setModel(new ProviderRepositoryFrameworkList());
|
||||
Response::setModel(new ProviderRepositoryRuntimeList());
|
||||
Response::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
|
||||
Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIST, 'frameworks', Response::MODEL_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
|
||||
@@ -373,7 +374,6 @@ Response::setModel(new Headers());
|
||||
Response::setModel(new Specification());
|
||||
Response::setModel(new Rule());
|
||||
Response::setModel(new Schedule());
|
||||
Response::setModel(new TemplateSMS());
|
||||
Response::setModel(new TemplateEmail());
|
||||
Response::setModel(new ConsoleVariables());
|
||||
Response::setModel(new MFAChallenge());
|
||||
|
||||
@@ -398,6 +398,11 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$register->set('telemetry.connectionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.open_connections'));
|
||||
$register->set('telemetry.connectionCreatedCounter', fn () => $telemetry->createCounter('realtime.server.connection.created'));
|
||||
$register->set('telemetry.messageSentCounter', fn () => $telemetry->createCounter('realtime.server.message.sent'));
|
||||
$register->set('telemetry.deliveryDelayHistogram', fn () => $telemetry->createHistogram(
|
||||
name: 'realtime.server.delivery_delay',
|
||||
unit: 'ms',
|
||||
advisory: ['ExplicitBucketBoundaries' => [100, 250, 500, 750, 1000, 1500, 2000, 3000, 5000, 7500, 10000, 15000, 30000]],
|
||||
));
|
||||
|
||||
$attempts = 0;
|
||||
$start = time();
|
||||
@@ -592,6 +597,20 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
if ($total > 0) {
|
||||
$register->get('telemetry.messageSentCounter')->add($total);
|
||||
$stats->incr($event['project'], 'messages', $total);
|
||||
$updatedAt = $event['data']['payload']['$updatedAt'] ?? null;
|
||||
if (\is_string($updatedAt)) {
|
||||
try {
|
||||
$updatedAtDate = new \DateTimeImmutable($updatedAt);
|
||||
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
$updatedAtTimestampMs = (float) $updatedAtDate->format('U.u') * 1000;
|
||||
$nowTimestampMs = (float) $now->format('U.u') * 1000;
|
||||
$delayMs = (int) \max(0, $nowTimestampMs - $updatedAtTimestampMs);
|
||||
|
||||
$register->get('telemetry.deliveryDelayHistogram')->record($delayMs);
|
||||
} catch (\Throwable) {
|
||||
// Ignore invalid timestamp payloads.
|
||||
}
|
||||
}
|
||||
|
||||
$projectId = $event['project'] ?? null;
|
||||
|
||||
|
||||
@@ -120,7 +120,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_SMTP_HOST
|
||||
- _APP_SMTP_PORT
|
||||
- _APP_SMTP_SECURE
|
||||
@@ -256,7 +255,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
@@ -287,7 +285,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-webhooks:
|
||||
@@ -315,7 +312,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -356,7 +352,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
@@ -416,7 +411,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-builds:
|
||||
@@ -453,7 +447,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
@@ -529,7 +522,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-executions:
|
||||
@@ -592,7 +584,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_SITES_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
@@ -630,7 +621,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -673,7 +663,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_SMS_FROM
|
||||
- _APP_SMS_PROVIDER
|
||||
@@ -734,7 +723,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
@@ -773,7 +761,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
@@ -806,7 +793,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -839,7 +825,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -871,7 +856,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -907,7 +891,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-executions:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -936,7 +919,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-messages:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -965,7 +947,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
<?php if ($enableAssistant): ?>
|
||||
appwrite-assistant:
|
||||
@@ -1068,13 +1049,12 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-mongodb:/data/db
|
||||
- appwrite-mongodb-keyfile:/data/keyfile
|
||||
ports:
|
||||
- "27017:27017"
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
|
||||
@@ -1205,7 +1185,6 @@ volumes:
|
||||
<?php elseif ($dbService === 'mongodb'): ?>
|
||||
appwrite-mongodb:
|
||||
appwrite-mongodb-keyfile:
|
||||
appwrite-mongodb-config:
|
||||
<?php endif; ?>
|
||||
appwrite-redis:
|
||||
appwrite-cache:
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.22.*",
|
||||
"utopia-php/migration": "1.9.*",
|
||||
"utopia-php/platform": "0.12.*",
|
||||
"utopia-php/platform": "0.13.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
|
||||
Generated
+19
-17
@@ -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": "f6a87c1012b316e614258f8f57a28e48",
|
||||
"content-hash": "c5ae97637fd0ec0a950044d1c33677ea",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -4325,16 +4325,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/image.git",
|
||||
"reference": "ce788ff0121a79286fdbe3ef3eba566de646df65"
|
||||
"reference": "9af2fcff028a42550465e2ccad88e3b31c3584f3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/image/zipball/ce788ff0121a79286fdbe3ef3eba566de646df65",
|
||||
"reference": "ce788ff0121a79286fdbe3ef3eba566de646df65",
|
||||
"url": "https://api.github.com/repos/utopia-php/image/zipball/9af2fcff028a42550465e2ccad88e3b31c3584f3",
|
||||
"reference": "9af2fcff028a42550465e2ccad88e3b31c3584f3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4343,10 +4343,12 @@
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
"phpstan/phpstan": "^1.10.0",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"vimeo/psalm": "4.13.1"
|
||||
"laravel/pint": "1.24.*",
|
||||
"phpstan/phpstan": "2.1.*",
|
||||
"phpunit/phpunit": "10.5.*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "Imagick extension is required for Imagick adapter"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -4368,9 +4370,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/image/issues",
|
||||
"source": "https://github.com/utopia-php/image/tree/0.8.4"
|
||||
"source": "https://github.com/utopia-php/image/tree/0.8.5"
|
||||
},
|
||||
"time": "2025-06-03T08:32:20+00:00"
|
||||
"time": "2026-04-17T15:02:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/locale",
|
||||
@@ -4642,16 +4644,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/platform",
|
||||
"version": "0.12.1",
|
||||
"version": "0.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/platform.git",
|
||||
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc"
|
||||
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
|
||||
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/d23af5349a7ea9ee11f9920a13626226f985522e",
|
||||
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4687,9 +4689,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/platform/issues",
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.12.1"
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.13.0"
|
||||
},
|
||||
"time": "2026-04-08T04:11:31+00:00"
|
||||
"time": "2026-04-17T09:57:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/pools",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Reset a custom SMS template to its default value. This endpoint removes any custom message and restores the template to its original state.
|
||||
@@ -1 +0,0 @@
|
||||
Get a custom SMS template for the specified locale and type returning it's contents.
|
||||
@@ -1 +0,0 @@
|
||||
Update a custom SMS template for the specified locale and type. Use this endpoint to modify the content of your SMS templates.
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -71,7 +71,9 @@ class Mails extends Listener
|
||||
throw new \Exception('Invalid template path');
|
||||
}
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])["email.sessionAlert-$event->locale"] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])["email.sessionAlert-" . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.sessionAlert-' . $locale->fallback] ?? [];
|
||||
$isBranded = $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE;
|
||||
|
||||
$subject = $customTemplate['subject'] ?? $locale->getText('emails.sessionAlert.subject');
|
||||
|
||||
@@ -170,11 +170,6 @@ class Create extends Action
|
||||
|
||||
$message = Template::fromFile($templatesPath . '/sms-base.tpl');
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['sms.mfaChallenge-' . $locale->default] ?? [];
|
||||
if (!empty($customTemplate)) {
|
||||
$message = $customTemplate['message'] ?? $message;
|
||||
}
|
||||
|
||||
$messageContent = Template::fromString($locale->getText("sms.verification.body"));
|
||||
$messageContent
|
||||
->setParam('{{project}}', $projectName)
|
||||
@@ -223,7 +218,9 @@ class Create extends Action
|
||||
$preview = $locale->getText("emails.mfaChallenge.preview");
|
||||
$heading = $locale->getText("emails.mfaChallenge.heading");
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->fallback] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
|
||||
@@ -31,7 +31,7 @@ class Get extends Action
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/download')
|
||||
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build/download', ['type' => 'output'])
|
||||
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build/download')
|
||||
->groups(['api', 'functions'])
|
||||
->desc('Get deployment download')
|
||||
->label('scope', 'functions.read')
|
||||
|
||||
@@ -144,7 +144,8 @@ class Builds extends Action
|
||||
$log,
|
||||
$executor,
|
||||
$plan,
|
||||
$platform
|
||||
$platform,
|
||||
(int) ($payload['timeout'] ?? System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900))
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -179,7 +180,8 @@ class Builds extends Action
|
||||
Log $log,
|
||||
Executor $executor,
|
||||
array $plan,
|
||||
array $platform
|
||||
array $platform,
|
||||
int $timeout
|
||||
): void {
|
||||
Console::info('Deployment action started');
|
||||
|
||||
@@ -592,10 +594,7 @@ class Builds extends Action
|
||||
|
||||
$cpus = $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT;
|
||||
$memory = max($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, $minMemory);
|
||||
$timeout = (int) System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900);
|
||||
|
||||
$jwtExpiry = (int) System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900);
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $timeout, 0);
|
||||
|
||||
$apiKey = $jwtObj->encode([
|
||||
'projectId' => $project->getId(),
|
||||
|
||||
+7
-6
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status;
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Protocols;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
@@ -22,16 +22,17 @@ class Update extends Action
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectProtocolStatus';
|
||||
return 'updateProjectProtocol';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/protocols/:protocolId/status')
|
||||
->setHttpPath('/v1/project/protocols/:protocolId')
|
||||
->httpAlias('/v1/project/protocols/:protocolId/status')
|
||||
->httpAlias('/v1/projects/:projectId/api')
|
||||
->desc('Update project protocol status')
|
||||
->desc('Update project protocol')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('event', 'protocols.[protocolId].update')
|
||||
@@ -40,9 +41,9 @@ class Update extends Action
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'updateProtocolStatus',
|
||||
name: 'updateProtocol',
|
||||
description: <<<EOT
|
||||
Update the status of a specific protocol. Use this endpoint to enable or disable a protocol in your project.
|
||||
Update properties of a specific protocol. Use this endpoint to enable or disable a protocol in your project.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
+7
-6
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Services\Status;
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Services;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
@@ -22,16 +22,17 @@ class Update extends Action
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectServiceStatus';
|
||||
return 'updateProjectService';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/services/:serviceId/status')
|
||||
->setHttpPath('/v1/project/services/:serviceId')
|
||||
->httpAlias('/v1/project/services/:serviceId/status')
|
||||
->httpAlias('/v1/projects/:projectId/service')
|
||||
->desc('Update project service status')
|
||||
->desc('Update project service')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('event', 'services.[serviceId].update')
|
||||
@@ -40,9 +41,9 @@ class Update extends Action
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'updateServiceStatus',
|
||||
name: 'updateService',
|
||||
description: <<<EOT
|
||||
Update the status of a specific service. Use this endpoint to enable or disable a service in your project.
|
||||
Update properties of a specific service. Use this endpoint to enable or disable a service in your project.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
@@ -22,8 +22,8 @@ use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Update as Updat
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Create as CreateWindowsPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Update as UpdateWindowsPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatforms;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status\Update as UpdateProjectProtocolStatus;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Services\Status\Update as UpdateProjectServiceStatus;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Protocols\Update as UpdateProjectProtocol;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Services\Update as UpdateProjectService;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
|
||||
@@ -42,8 +42,8 @@ class Http extends Service
|
||||
|
||||
// Project
|
||||
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
|
||||
$this->addAction(UpdateProjectProtocolStatus::getName(), new UpdateProjectProtocolStatus());
|
||||
$this->addAction(UpdateProjectServiceStatus::getName(), new UpdateProjectServiceStatus());
|
||||
$this->addAction(UpdateProjectProtocol::getName(), new UpdateProjectProtocol());
|
||||
$this->addAction(UpdateProjectService::getName(), new UpdateProjectService());
|
||||
|
||||
// Variables
|
||||
$this->addAction(CreateVariable::getName(), new CreateVariable());
|
||||
|
||||
@@ -54,7 +54,7 @@ class Get extends Action
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'bucket/{request.bucketId}')
|
||||
->label('cache.resource', 'file/{request.fileId}')
|
||||
->label('cache.params', ['width', 'height', 'gravity', 'quality', 'borderWidth', 'borderColor', 'borderRadius', 'opacity', 'rotation', 'background', 'output'])
|
||||
->label('cache.params', ['width', 'height', 'gravity', 'quality', 'borderWidth', 'borderColor', 'borderRadius', 'opacity', 'rotation', 'background', 'output', 'project'])
|
||||
->label('sdk', new Method(
|
||||
namespace: 'storage',
|
||||
group: 'files',
|
||||
|
||||
@@ -324,7 +324,9 @@ class Create extends Action
|
||||
$body = $locale->getText('emails.invitation.body');
|
||||
$preview = $locale->getText('emails.invitation.preview');
|
||||
$subject = $locale->getText('emails.invitation.subject');
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.invitation-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.invitation-' . $locale->fallback] ?? [];
|
||||
|
||||
$message = Template::fromFile(APP_CE_CONFIG_DIR . '/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
@@ -407,11 +409,6 @@ class Create extends Action
|
||||
|
||||
$message = Template::fromFile(APP_CE_CONFIG_DIR . '/locale/templates/sms-base.tpl');
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? [];
|
||||
if (! empty($customTemplate)) {
|
||||
$message = $customTemplate['message'];
|
||||
}
|
||||
|
||||
$message = $message->setParam('{{token}}', $url);
|
||||
$message = $message->render();
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ class Delete extends Action
|
||||
if ($team->getAttribute('userInternalId') === $membership->getAttribute('userInternalId')) {
|
||||
$membership = $dbForProject->findOne('memberships', [
|
||||
Query::equal('teamInternalId', [$team->getSequence()]),
|
||||
Query::equal('confirm', [true]),
|
||||
]);
|
||||
|
||||
if (!$membership->isEmpty()) {
|
||||
|
||||
+1
@@ -307,6 +307,7 @@ class Create extends Action
|
||||
];
|
||||
}
|
||||
|
||||
$output->setAttribute('type', $type);
|
||||
$output->setAttribute('variables', $variables);
|
||||
|
||||
$response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME);
|
||||
|
||||
@@ -313,6 +313,7 @@ class XList extends Action
|
||||
}, $repos);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'type' => $type,
|
||||
$type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos,
|
||||
'total' => $total,
|
||||
]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST);
|
||||
|
||||
@@ -263,6 +263,182 @@ abstract class Format
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Model> $models
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
protected function getDiscriminator(array $models, string $refPrefix): ?array
|
||||
{
|
||||
if (\count($models) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$candidateKeys = \array_keys($models[0]->conditions);
|
||||
|
||||
foreach (\array_slice($models, 1) as $model) {
|
||||
$candidateKeys = \array_values(\array_intersect($candidateKeys, \array_keys($model->conditions)));
|
||||
}
|
||||
|
||||
if (empty($candidateKeys)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($candidateKeys as $key) {
|
||||
$mapping = [];
|
||||
$isValid = true;
|
||||
|
||||
foreach ($models as $model) {
|
||||
$rules = $model->getRules();
|
||||
$condition = $model->conditions[$key] ?? null;
|
||||
|
||||
if (!isset($rules[$key]) || ($rules[$key]['required'] ?? false) !== true) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!\is_array($condition)) {
|
||||
if (!\is_scalar($condition)) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$values = [$condition];
|
||||
} else {
|
||||
if ($condition === []) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$values = $condition;
|
||||
$hasInvalidValue = false;
|
||||
|
||||
foreach ($values as $value) {
|
||||
if (!\is_scalar($value)) {
|
||||
$hasInvalidValue = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasInvalidValue) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($rules[$key]['enum']) && \is_array($rules[$key]['enum'])) {
|
||||
$values = \array_values(\array_filter(
|
||||
$values,
|
||||
fn (mixed $value) => \in_array($value, $rules[$key]['enum'], true)
|
||||
));
|
||||
}
|
||||
|
||||
if ($values === []) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$ref = $refPrefix . $model->getType();
|
||||
|
||||
foreach ($values as $value) {
|
||||
$mappingKey = \is_bool($value) ? ($value ? 'true' : 'false') : (string) $value;
|
||||
|
||||
if (isset($mapping[$mappingKey]) && $mapping[$mappingKey] !== $ref) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$mapping[$mappingKey] = $ref;
|
||||
}
|
||||
|
||||
if (!$isValid) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isValid || $mapping === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return [
|
||||
'propertyName' => $key,
|
||||
'mapping' => $mapping,
|
||||
];
|
||||
}
|
||||
|
||||
// Single-key failed — try compound discriminator
|
||||
return $this->getCompoundDiscriminator($models, $refPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Model> $models
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function getCompoundDiscriminator(array $models, string $refPrefix): ?array
|
||||
{
|
||||
$allKeys = [];
|
||||
foreach ($models as $model) {
|
||||
foreach (\array_keys($model->conditions) as $key) {
|
||||
if (!\in_array($key, $allKeys, true)) {
|
||||
$allKeys[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($allKeys) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$primaryKey = $allKeys[0];
|
||||
$primaryMapping = [];
|
||||
$compoundMapping = [];
|
||||
|
||||
foreach ($models as $model) {
|
||||
$rules = $model->getRules();
|
||||
$conditions = [];
|
||||
|
||||
foreach ($model->conditions as $key => $condition) {
|
||||
if (!isset($rules[$key]) || ($rules[$key]['required'] ?? false) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!\is_scalar($condition)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$conditions[$key] = \is_bool($condition) ? ($condition ? 'true' : 'false') : (string) $condition;
|
||||
}
|
||||
|
||||
if (empty($conditions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ref = $refPrefix . $model->getType();
|
||||
$compoundMapping[$ref] = $conditions;
|
||||
|
||||
// Best-effort single-key mapping — last model with this value wins (fallback)
|
||||
if (isset($conditions[$primaryKey])) {
|
||||
$primaryMapping[$conditions[$primaryKey]] = $ref;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify compound uniqueness
|
||||
$seen = [];
|
||||
foreach ($compoundMapping as $conditions) {
|
||||
$sig = \json_encode($conditions, JSON_THROW_ON_ERROR);
|
||||
if (isset($seen[$sig])) {
|
||||
return null;
|
||||
}
|
||||
$seen[$sig] = true;
|
||||
}
|
||||
|
||||
return \array_filter([
|
||||
'propertyName' => $primaryKey,
|
||||
'mapping' => !empty($primaryMapping) ? $primaryMapping : null,
|
||||
'x-propertyNames' => $allKeys,
|
||||
'x-mapping' => $compoundMapping,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getRequestEnumName(string $service, string $method, string $param): ?string
|
||||
{
|
||||
/* `$service` is `$namespace` */
|
||||
@@ -595,16 +771,6 @@ abstract class Format
|
||||
return 'EmailTemplateLocale';
|
||||
}
|
||||
break;
|
||||
case 'getSmsTemplate':
|
||||
case 'updateSmsTemplate':
|
||||
case 'deleteSmsTemplate':
|
||||
switch ($param) {
|
||||
case 'type':
|
||||
return 'SmsTemplateType';
|
||||
case 'locale':
|
||||
return 'SmsTemplateLocale';
|
||||
}
|
||||
break;
|
||||
case 'createPlatform':
|
||||
switch ($param) {
|
||||
case 'type':
|
||||
|
||||
@@ -316,9 +316,10 @@ class OpenAPI3 extends Format
|
||||
'description' => $modelDescription,
|
||||
'content' => [
|
||||
$produces => [
|
||||
'schema' => [
|
||||
'oneOf' => \array_map(fn ($m) => ['$ref' => '#/components/schemas/' . $m->getType()], $model)
|
||||
],
|
||||
'schema' => \array_filter([
|
||||
'oneOf' => \array_map(fn ($m) => ['$ref' => '#/components/schemas/' . $m->getType()], $model),
|
||||
'discriminator' => $this->getDiscriminator($model, '#/components/schemas/'),
|
||||
]),
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -900,18 +901,30 @@ class OpenAPI3 extends Format
|
||||
$rule['type'] = ($rule['type']) ? $rule['type'] : 'none';
|
||||
|
||||
if (\is_array($rule['type'])) {
|
||||
$resolvedModels = \array_map(function (string $type) {
|
||||
foreach ($this->models as $model) {
|
||||
if ($model->getType() === $type) {
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException("Unresolved model '{$type}'. Ensure the model is registered.");
|
||||
}, $rule['type']);
|
||||
|
||||
if ($rule['array']) {
|
||||
$items = [
|
||||
$items = \array_filter([
|
||||
'anyOf' => \array_map(function ($type) {
|
||||
return ['$ref' => '#/components/schemas/' . $type];
|
||||
}, $rule['type'])
|
||||
];
|
||||
}, $rule['type']),
|
||||
'discriminator' => $this->getDiscriminator($resolvedModels, '#/components/schemas/'),
|
||||
]);
|
||||
} else {
|
||||
$items = [
|
||||
$items = \array_filter([
|
||||
'oneOf' => \array_map(function ($type) {
|
||||
return ['$ref' => '#/components/schemas/' . $type];
|
||||
}, $rule['type'])
|
||||
];
|
||||
}, $rule['type']),
|
||||
'discriminator' => $this->getDiscriminator($resolvedModels, '#/components/schemas/'),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$items = [
|
||||
|
||||
@@ -322,11 +322,12 @@ class Swagger2 extends Format
|
||||
}
|
||||
$temp['responses'][(string)$response->getCode() ?? '500'] = [
|
||||
'description' => $modelDescription,
|
||||
'schema' => [
|
||||
'schema' => \array_filter([
|
||||
'x-oneOf' => \array_map(function ($m) {
|
||||
return ['$ref' => '#/definitions/' . $m->getType()];
|
||||
}, $model)
|
||||
],
|
||||
}, $model),
|
||||
'x-discriminator' => $this->getDiscriminator($model, '#/definitions/'),
|
||||
]),
|
||||
];
|
||||
} else {
|
||||
// Response definition using one type
|
||||
@@ -880,14 +881,27 @@ class Swagger2 extends Format
|
||||
$rule['type'] = ($rule['type']) ?: 'none';
|
||||
|
||||
if (\is_array($rule['type'])) {
|
||||
$resolvedModels = \array_map(function (string $type) {
|
||||
foreach ($this->models as $model) {
|
||||
if ($model->getType() === $type) {
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException("Unresolved model '{$type}'. Ensure the model is registered.");
|
||||
}, $rule['type']);
|
||||
$xDiscriminator = $this->getDiscriminator($resolvedModels, '#/definitions/');
|
||||
|
||||
if ($rule['array']) {
|
||||
$items = [
|
||||
'x-anyOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type'])
|
||||
];
|
||||
$items = \array_filter([
|
||||
'x-anyOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type']),
|
||||
'x-discriminator' => $xDiscriminator,
|
||||
]);
|
||||
} else {
|
||||
$items = [
|
||||
'x-oneOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type'])
|
||||
];
|
||||
$items = \array_filter([
|
||||
'x-oneOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type']),
|
||||
'x-discriminator' => $xDiscriminator,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$items = [
|
||||
|
||||
@@ -238,6 +238,9 @@ class Request extends UtopiaRequest
|
||||
if ($allowedParams !== null) {
|
||||
$params = array_intersect_key($params, array_flip($allowedParams));
|
||||
}
|
||||
if (!isset($params['project'])) {
|
||||
$params['project'] = $this->getHeader('x-appwrite-project', '');
|
||||
}
|
||||
ksort($params);
|
||||
return md5($this->getURI() . '*' . serialize($params) . '*' . APP_CACHE_BUSTER);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,13 @@ class V19 extends Filter
|
||||
case 'functions.updateVariable':
|
||||
$content['secret'] = false;
|
||||
break;
|
||||
case 'functions.getDeploymentDownload':
|
||||
// Pre-1.7.0 clients call the legacy alias
|
||||
// `/v1/functions/:functionId/deployments/:deploymentId/build/download`,
|
||||
// which always downloaded the build output. The merged 1.7.0 endpoint
|
||||
// requires an explicit `type` param, so force it to `output` here.
|
||||
$content['type'] = 'output';
|
||||
break;
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
@@ -73,10 +73,10 @@ class V22 extends Filter
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
switch ($model) {
|
||||
case 'project.updateServiceStatus':
|
||||
case 'project.updateService':
|
||||
$content = $this->parseUpdateServiceStatus($content);
|
||||
break;
|
||||
case 'project.updateProtocolStatus':
|
||||
case 'project.updateProtocol':
|
||||
$content = $this->parseUpdateProtocolStatus($content);
|
||||
break;
|
||||
case 'project.createKey':
|
||||
|
||||
@@ -265,7 +265,6 @@ class Response extends SwooleResponse
|
||||
public const MODEL_VARIABLE = 'variable';
|
||||
public const MODEL_VARIABLE_LIST = 'variableList';
|
||||
public const MODEL_VCS = 'vcs';
|
||||
public const MODEL_SMS_TEMPLATE = 'smsTemplate';
|
||||
public const MODEL_EMAIL_TEMPLATE = 'emailTemplate';
|
||||
|
||||
// Health
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoArgon2 extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'argon2',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// No options if imported. If hashed by Appwrite, following configuration is available:
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoBcrypt extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'bcrypt',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoMd5 extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'md5',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoPhpass extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'phpass',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoScrypt extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'scrypt',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoScryptModified extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'scryptMod',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoSha extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'sha',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
|
||||
@@ -7,9 +7,16 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
abstract class Detection extends Model
|
||||
{
|
||||
public function __construct()
|
||||
public function __construct(string $type)
|
||||
{
|
||||
$this
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_ENUM,
|
||||
'description' => 'Repository detection type.',
|
||||
'default' => $type,
|
||||
'example' => $type,
|
||||
'enum' => [$type],
|
||||
])
|
||||
->addRule('variables', [
|
||||
'type' => Response::MODEL_DETECTION_VARIABLE,
|
||||
'description' => 'Environment variables found in .env files',
|
||||
|
||||
@@ -8,7 +8,11 @@ class DetectionFramework extends Detection
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->conditions = [
|
||||
'type' => 'framework',
|
||||
];
|
||||
|
||||
parent::__construct('framework');
|
||||
|
||||
$this
|
||||
->addRule('framework', [
|
||||
|
||||
@@ -8,7 +8,11 @@ class DetectionRuntime extends Detection
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->conditions = [
|
||||
'type' => 'runtime',
|
||||
];
|
||||
|
||||
parent::__construct('runtime');
|
||||
|
||||
$this
|
||||
->addRule('runtime', [
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
|
||||
class ProviderRepositoryFrameworkList extends BaseList
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'framework',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
'Framework Provider Repositories List',
|
||||
Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST,
|
||||
'frameworkProviderRepositories',
|
||||
Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK
|
||||
);
|
||||
|
||||
$this->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Provider repository list type.',
|
||||
'default' => 'framework',
|
||||
'example' => 'framework',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
|
||||
class ProviderRepositoryRuntimeList extends BaseList
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'runtime',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
'Runtime Provider Repositories List',
|
||||
Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST,
|
||||
'runtimeProviderRepositories',
|
||||
Response::MODEL_PROVIDER_REPOSITORY_RUNTIME
|
||||
);
|
||||
|
||||
$this->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Provider repository list type.',
|
||||
'default' => 'runtime',
|
||||
'example' => 'runtime',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
|
||||
class TemplateSMS extends Template
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'SmsTemplate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_SMS_TEMPLATE;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,12 @@ class AccountConsoleClientTest extends Scope
|
||||
use ProjectConsole;
|
||||
use SideClient;
|
||||
|
||||
public function testDeleteAccount(): void
|
||||
/**
|
||||
* Test that account deletion succeeds even with active team memberships.
|
||||
* When the user is the sole owner and only member of a team, the team
|
||||
* should be cleaned up automatically.
|
||||
*/
|
||||
public function testDeleteAccountWithMembership(): void
|
||||
{
|
||||
$email = uniqid() . 'user@localhost.test';
|
||||
$password = 'password';
|
||||
@@ -46,7 +51,7 @@ class AccountConsoleClientTest extends Scope
|
||||
|
||||
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
|
||||
|
||||
// create team
|
||||
// Create team — user becomes sole owner and only member
|
||||
$team = $this->client->call(Client::METHOD_POST, '/teams', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
@@ -58,7 +63,51 @@ class AccountConsoleClientTest extends Scope
|
||||
]);
|
||||
$this->assertEquals($team['headers']['status-code'], 201);
|
||||
|
||||
$teamId = $team['body']['$id'];
|
||||
// Account deletion should succeed even with active membership
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that account deletion works when the user has no team memberships.
|
||||
*/
|
||||
public function testDeleteAccountWithoutMembership(): void
|
||||
{
|
||||
$email = uniqid() . 'user@localhost.test';
|
||||
$password = 'password';
|
||||
$name = 'User Name';
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'name' => $name,
|
||||
]);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 201);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 201);
|
||||
|
||||
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
@@ -67,27 +116,7 @@ class AccountConsoleClientTest extends Scope
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 400);
|
||||
|
||||
// DELETE TEAM
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId, array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
$this->assertEquals($response['headers']['status-code'], 204);
|
||||
|
||||
$this->assertEventually(function () use ($session) {
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
}, 10_000, 500);
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testSessionAlert(): void
|
||||
|
||||
@@ -241,6 +241,33 @@ trait ProtocolsBase
|
||||
$this->assertSame(404, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
|
||||
public function testUpdateProtocolLegacyStatusPath(): void
|
||||
{
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders());
|
||||
|
||||
// Disable via the legacy `/status` alias
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/project/protocols/rest/status', $headers, [
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertSame(false, $response['body']['protocolStatusForRest']);
|
||||
|
||||
// Re-enable via the legacy `/status` alias
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/project/protocols/rest/status', $headers, [
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame(true, $response['body']['protocolStatusForRest']);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
protected function updateProtocolStatus(string $protocolId, bool $enabled, bool $authenticated = true): mixed
|
||||
@@ -254,7 +281,7 @@ trait ProtocolsBase
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_PATCH, '/project/protocols/' . $protocolId . '/status', $headers, [
|
||||
return $this->client->call(Client::METHOD_PATCH, '/project/protocols/' . $protocolId, $headers, [
|
||||
'enabled' => $enabled,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -239,6 +239,33 @@ trait ServicesBase
|
||||
$this->assertSame(404, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
|
||||
public function testUpdateServiceLegacyStatusPath(): void
|
||||
{
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders());
|
||||
|
||||
// Disable via the legacy `/status` alias
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/project/services/teams/status', $headers, [
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertSame(false, $response['body']['serviceStatusForTeams']);
|
||||
|
||||
// Re-enable via the legacy `/status` alias
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/project/services/teams/status', $headers, [
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame(true, $response['body']['serviceStatusForTeams']);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
protected function updateServiceStatus(string $serviceId, bool $enabled, bool $authenticated = true): mixed
|
||||
@@ -252,7 +279,7 @@ trait ServicesBase
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_PATCH, '/project/services/' . $serviceId . '/status', $headers, [
|
||||
return $this->client->call(Client::METHOD_PATCH, '/project/services/' . $serviceId, $headers, [
|
||||
'enabled' => $enabled,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1161,42 +1161,220 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertEquals('verification', $response['body']['type']);
|
||||
$this->assertEquals('en-us', $response['body']['locale']);
|
||||
$this->assertEquals('Please verify your email {{url}}', $response['body']['message']);
|
||||
}
|
||||
|
||||
// Temporary disabled until implemented
|
||||
// /** Get Default SMS Template */
|
||||
// $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([
|
||||
// 'content-type' => 'application/json',
|
||||
// 'x-appwrite-project' => $this->getProject()['$id'],
|
||||
// ], $this->getHeaders()));
|
||||
#[Group('smtpAndTemplates')]
|
||||
public function testSessionAlertLocaleFallback(): void
|
||||
{
|
||||
$smtpHost = 'maildev';
|
||||
$smtpPort = 1025;
|
||||
$smtpUsername = 'user';
|
||||
$smtpPassword = 'password';
|
||||
|
||||
// $this->assertEquals(200, $response['headers']['status-code']);
|
||||
// $this->assertEquals('verification', $response['body']['type']);
|
||||
// $this->assertEquals('en-us', $response['body']['locale']);
|
||||
// $this->assertEquals('{{token}}', $response['body']['message']);
|
||||
/** Create team */
|
||||
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'teamId' => ID::unique(),
|
||||
'name' => 'Session Alert Locale Fallback Test Team',
|
||||
]);
|
||||
$this->assertEquals(201, $team['headers']['status-code']);
|
||||
$teamId = $team['body']['$id'];
|
||||
|
||||
// /** Update SMS template */
|
||||
// $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([
|
||||
// 'content-type' => 'application/json',
|
||||
// 'x-appwrite-project' => $this->getProject()['$id'],
|
||||
// ], $this->getHeaders()), [
|
||||
// 'message' => 'Please verify your email {{token}}',
|
||||
// ]);
|
||||
/** Create project */
|
||||
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'Session Alert Locale Fallback Test',
|
||||
'teamId' => $teamId,
|
||||
'region' => System::getEnv('_APP_REGION', 'default'),
|
||||
]);
|
||||
$this->assertEquals(201, $project['headers']['status-code']);
|
||||
$projectId = $project['body']['$id'];
|
||||
|
||||
// $this->assertEquals(200, $response['headers']['status-code']);
|
||||
// $this->assertEquals('verification', $response['body']['type']);
|
||||
// $this->assertEquals('en-us', $response['body']['locale']);
|
||||
// $this->assertEquals('Please verify your email {{token}}', $response['body']['message']);
|
||||
/** Configure custom SMTP pointing to maildev */
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/smtp', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => true,
|
||||
'senderEmail' => 'mailer@appwrite.io',
|
||||
'senderName' => 'Mailer',
|
||||
'host' => $smtpHost,
|
||||
'port' => $smtpPort,
|
||||
'username' => $smtpUsername,
|
||||
'password' => $smtpPassword,
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
// /** Get Updated SMS Template */
|
||||
// $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([
|
||||
// 'content-type' => 'application/json',
|
||||
// 'x-appwrite-project' => $this->getProject()['$id'],
|
||||
// ], $this->getHeaders()));
|
||||
/**
|
||||
* Set custom sessionAlert template with no explicit locale.
|
||||
* When locale is omitted, the server stores it under the request's
|
||||
* default locale (en), which is the same slot used as the system-wide
|
||||
* fallback when a session's locale has no dedicated template.
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/templates/email', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'type' => 'sessionAlert',
|
||||
// Intentionally no locale
|
||||
'subject' => 'Fallback sign-in alert',
|
||||
'message' => 'Fallback sign-in alert body',
|
||||
'senderName' => 'Fallback Mailer',
|
||||
'senderEmail' => 'fallback@appwrite.io',
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('Fallback sign-in alert', $response['body']['subject']);
|
||||
$this->assertEquals('Fallback sign-in alert body', $response['body']['message']);
|
||||
$this->assertEquals('Fallback Mailer', $response['body']['senderName']);
|
||||
$this->assertEquals('fallback@appwrite.io', $response['body']['senderEmail']);
|
||||
|
||||
// $this->assertEquals(200, $response['headers']['status-code']);
|
||||
// $this->assertEquals('verification', $response['body']['type']);
|
||||
// $this->assertEquals('en-us', $response['body']['locale']);
|
||||
// $this->assertEquals('Please verify your email {{token}}', $response['body']['message']);
|
||||
/** Set custom sessionAlert template for Slovak locale */
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/templates/email', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'type' => 'sessionAlert',
|
||||
'locale' => 'sk',
|
||||
'subject' => 'Slovak sign-in alert',
|
||||
'message' => 'Slovak sign-in alert body',
|
||||
'senderName' => 'Slovak Mailer',
|
||||
'senderEmail' => 'sk@appwrite.io',
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('Slovak sign-in alert', $response['body']['subject']);
|
||||
$this->assertEquals('Slovak sign-in alert body', $response['body']['message']);
|
||||
$this->assertEquals('Slovak Mailer', $response['body']['senderName']);
|
||||
$this->assertEquals('sk@appwrite.io', $response['body']['senderEmail']);
|
||||
|
||||
/** Enable session alerts */
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/auth/session-alerts', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'alerts' => true,
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
/** Verify alerts are enabled */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertTrue($response['body']['authSessionAlerts']);
|
||||
|
||||
/** Create user (email + password) in the project */
|
||||
$userEmail = 'session-alert-' . uniqid() . '@appwrite.io';
|
||||
$password = 'password';
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $userEmail,
|
||||
'password' => $password,
|
||||
'name' => 'Session Alert User',
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Prime first session — the listener suppresses the alert on the very
|
||||
* first session of a user, so this session is setup only.
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], [
|
||||
'email' => $userEmail,
|
||||
'password' => $password,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
/** Create a new session with no locale — expect fallback (en) template */
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], [
|
||||
'email' => $userEmail,
|
||||
'password' => $password,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Emails are delivered asynchronously via the mail queue, so maildev may
|
||||
* still be catching up. The probe callback forces getLastEmailByAddress
|
||||
* to keep polling until an email matching the expected `from` address
|
||||
* appears — i.e. we await the new email rather than returning an older
|
||||
* one already in the inbox from a previous session.
|
||||
*/
|
||||
$lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) {
|
||||
$this->assertEquals('fallback@appwrite.io', $email['from'][0]['address']);
|
||||
});
|
||||
$this->assertEquals('Fallback sign-in alert', $lastEmail['subject']);
|
||||
$this->assertEquals('Fallback Mailer', $lastEmail['from'][0]['name']);
|
||||
$this->assertStringContainsString('Fallback sign-in alert body', $lastEmail['html']);
|
||||
|
||||
/** Create a new session with German locale — expect fallback (en) template */
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-locale' => 'de',
|
||||
], [
|
||||
'email' => $userEmail,
|
||||
'password' => $password,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
/** Probe on `from` address ensures we await a fallback-shaped email */
|
||||
$lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) {
|
||||
$this->assertEquals('fallback@appwrite.io', $email['from'][0]['address']);
|
||||
});
|
||||
$this->assertEquals('Fallback sign-in alert', $lastEmail['subject']);
|
||||
$this->assertEquals('Fallback Mailer', $lastEmail['from'][0]['name']);
|
||||
$this->assertStringContainsString('Fallback sign-in alert body', $lastEmail['html']);
|
||||
|
||||
/** Create a new session with Slovak locale — expect Slovak template */
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-locale' => 'sk',
|
||||
], [
|
||||
'email' => $userEmail,
|
||||
'password' => $password,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
/** Probe on `from` address ensures we await the Slovak email specifically */
|
||||
$lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) {
|
||||
$this->assertEquals('sk@appwrite.io', $email['from'][0]['address']);
|
||||
});
|
||||
$this->assertEquals('Slovak sign-in alert', $lastEmail['subject']);
|
||||
$this->assertEquals('Slovak Mailer', $lastEmail['from'][0]['name']);
|
||||
$this->assertStringContainsString('Slovak sign-in alert body', $lastEmail['html']);
|
||||
|
||||
/** Cleanup — delete the project */
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $projectId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
|
||||
/** Cleanup — delete the team */
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateProjectAuthDuration(): void
|
||||
|
||||
@@ -1074,6 +1074,28 @@ trait StorageBase
|
||||
$this->assertEquals(404, $file['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testFilePreviewAvifPublic(): void
|
||||
{
|
||||
$data = $this->setupBucketFile();
|
||||
$bucketId = $data['bucketId'];
|
||||
$fileId = $data['fileId'];
|
||||
$projectId = $this->getProject()['$id'];
|
||||
|
||||
// Matches the customer's URL pattern: no headers, project + output in query string only
|
||||
$preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview', [
|
||||
'content-type' => 'application/json',
|
||||
], [
|
||||
'project' => $projectId,
|
||||
'width' => 1080,
|
||||
'quality' => 40,
|
||||
'output' => 'avif',
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $preview['headers']['status-code']);
|
||||
$this->assertEquals('image/avif', $preview['headers']['content-type']);
|
||||
$this->assertNotEmpty($preview['body']);
|
||||
}
|
||||
|
||||
public function testFilePreview(): void
|
||||
{
|
||||
$data = $this->setupBucketFile();
|
||||
@@ -1093,6 +1115,49 @@ trait StorageBase
|
||||
$this->assertEquals(200, $preview['headers']['status-code']);
|
||||
$this->assertEquals('image/webp', $preview['headers']['content-type']);
|
||||
$this->assertNotEmpty($preview['body']);
|
||||
|
||||
// Preview PNG as avif
|
||||
$avifPreview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'width' => 1080,
|
||||
'quality' => 40,
|
||||
'output' => 'avif',
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $avifPreview['headers']['status-code']);
|
||||
$this->assertEquals('image/avif', $avifPreview['headers']['content-type']);
|
||||
$this->assertNotEmpty($avifPreview['body']);
|
||||
|
||||
// Preview JPEG as avif
|
||||
$jpegFile = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([
|
||||
'content-type' => 'multipart/form-data',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'fileId' => ID::unique(),
|
||||
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/disk-a/kitten-1.jpg'), 'image/jpeg', 'kitten-1.jpg'),
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $jpegFile['headers']['status-code']);
|
||||
|
||||
$avifFromJpeg = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $jpegFile['body']['$id'] . '/preview', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'width' => 1080,
|
||||
'quality' => 40,
|
||||
'output' => 'avif',
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $avifFromJpeg['headers']['status-code']);
|
||||
$this->assertEquals('image/avif', $avifFromJpeg['headers']['content-type']);
|
||||
$this->assertNotEmpty($avifFromJpeg['body']);
|
||||
}
|
||||
|
||||
public function testDeletePartiallyUploadedFile(): void
|
||||
|
||||
Reference in New Issue
Block a user