merge master

This commit is contained in:
Prateek Banga
2023-09-12 20:53:16 +05:30
parent 40f419663c
commit cbea4264ea
3359 changed files with 64705 additions and 6301 deletions
+240 -340
View File
@@ -1,19 +1,20 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Validator\Event;
use Appwrite\Network\Validator\CNAME;
use Utopia\Validator\Domain as DomainValidator;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Origin;
use Utopia\Validator\URL;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\ProjectId;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Response;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -21,26 +22,22 @@ use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Query;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Utopia\Cache\Cache;
use Utopia\Locale\Locale;
use Utopia\Pools\Group;
use Utopia\Registry\Registry;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
use Appwrite\Template\Template;
use Utopia\Locale\Locale;
use PHPMailer\PHPMailer\PHPMailer;
App::init()
->groups(['projects'])
@@ -64,7 +61,7 @@ App::post('/v1/projects')
->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.')
->param('name', null, new Text(128), 'Project name. Max length: 128 chars.')
->param('teamId', '', new UID(), 'Team unique ID.')
->param('region', App::getEnv('_APP_REGION', 'default'), new Whitelist(array_keys(array_filter(Config::getParam('regions'), fn($config) => !$config['disabled']))), 'Project Region.', true)
->param('region', App::getEnv('_APP_REGION', 'default'), new Whitelist(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true)
->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true)
->param('logo', '', new Text(1024), 'Project logo.', true)
->param('url', '', new URL(), 'Project URL.', true)
@@ -99,6 +96,7 @@ App::post('/v1/projects')
$backups['database_db_fra1_03'] = ['from' => '10:30', 'to' => '11:15'];
$backups['database_db_fra1_04'] = ['from' => '13:30', 'to' => '14:15'];
$backups['database_db_fra1_05'] = ['from' => '4:30', 'to' => '5:15'];
$backups['database_db_fra1_06'] = ['from' => '16:30', 'to' => '17:15'];
$databases = Config::getParam('pools-database', []);
@@ -122,7 +120,7 @@ App::post('/v1/projects')
}
}
if ($index = array_search('database_db_fra1_05', $databases)) {
if ($index = array_search('database_db_fra1_06', $databases)) {
$database = $databases[$index];
} else {
$database = $databases[array_rand($databases)];
@@ -161,7 +159,6 @@ App::post('/v1/projects')
'authProviders' => [],
'webhooks' => null,
'keys' => null,
'domains' => null,
'auths' => $auths,
'search' => implode(' ', [$projectId, $name]),
'database' => $database
@@ -245,7 +242,9 @@ App::get('/v1/projects')
}
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
@@ -291,6 +290,120 @@ App::get('/v1/projects/:projectId')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::get('/v1/projects/:projectId/usage')
->desc('Get usage stats for a project')
->groups(['api', 'projects', 'usage'])
->label('scope', 'projects.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'getUsage')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForConsole')
->inject('dbForProject')
->inject('register')
->action(function (string $projectId, string $range, Response $response, Database $dbForConsole, Database $dbForProject, Registry $register) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$usage = [];
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$periods = [
'24h' => [
'period' => '1h',
'limit' => 24,
],
'7d' => [
'period' => '1d',
'limit' => 7,
],
'30d' => [
'period' => '1d',
'limit' => 30,
],
'90d' => [
'period' => '1d',
'limit' => 90,
],
];
$dbForProject->setNamespace("_{$project->getInternalId()}");
$metrics = [
'project.$all.network.requests',
'project.$all.network.bandwidth',
'project.$all.storage.size',
'users.$all.count.total',
'databases.$all.count.total',
'documents.$all.count.total',
'executions.$all.compute.total',
'buckets.$all.count.total'
];
$stats = [];
Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
$requestDocs = $dbForProject->find('stats', [
Query::equal('period', [$period]),
Query::equal('metric', [$metric]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
// backfill metrics with empty values for graphs
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match ($period) { // convert period to seconds for unix timestamp math
'1h' => 3600,
'1d' => 86400,
};
$stats[$metric][] = [
'value' => 0,
'date' => DateTime::formatTz(DateTime::addSeconds(new \DateTime($stats[$metric][$last]['date'] ?? null), -1 * $diff)),
];
$backfill--;
}
$stats[$metric] = array_reverse($stats[$metric]);
}
});
$usage = new Document([
'range' => $range,
'requests' => $stats[$metrics[0]] ?? [],
'network' => $stats[$metrics[1]] ?? [],
'storage' => $stats[$metrics[2]] ?? [],
'users' => $stats[$metrics[3]] ?? [],
'databases' => $stats[$metrics[4]] ?? [],
'documents' => $stats[$metrics[5]] ?? [],
'executions' => $stats[$metrics[6]] ?? [],
'buckets' => $stats[$metrics[7]] ?? [],
]);
}
$response->dynamic($usage, Response::MODEL_USAGE_PROJECT);
});
App::patch('/v1/projects/:projectId')
->desc('Update Project')
->groups(['api', 'projects'])
@@ -323,17 +436,17 @@ App::patch('/v1/projects/:projectId')
}
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('name', $name)
->setAttribute('description', $description)
->setAttribute('logo', $logo)
->setAttribute('url', $url)
->setAttribute('legalName', $legalName)
->setAttribute('legalCountry', $legalCountry)
->setAttribute('legalState', $legalState)
->setAttribute('legalCity', $legalCity)
->setAttribute('legalAddress', $legalAddress)
->setAttribute('legalTaxId', $legalTaxId)
->setAttribute('search', implode(' ', [$projectId, $name])));
->setAttribute('name', $name)
->setAttribute('description', $description)
->setAttribute('logo', $logo)
->setAttribute('url', $url)
->setAttribute('legalName', $legalName)
->setAttribute('legalCountry', $legalCountry)
->setAttribute('legalState', $legalState)
->setAttribute('legalCity', $legalCity)
->setAttribute('legalAddress', $legalAddress)
->setAttribute('legalTaxId', $legalTaxId)
->setAttribute('search', implode(' ', [$projectId, $name])));
$response->dynamic($project, Response::MODEL_PROJECT);
});
@@ -366,14 +479,14 @@ App::patch('/v1/projects/:projectId/team')
}
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('teamId', $teamId)
->setAttribute('$permissions', [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
]));
->setAttribute('teamId', $teamId)
->setAttribute('$permissions', [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
]));
$response->dynamic($project, Response::MODEL_PROJECT);
});
@@ -389,7 +502,7 @@ App::patch('/v1/projects/:projectId/service')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn($element) => $element['optional'])), true), 'Service name.')
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name.')
->param('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForConsole')
@@ -431,7 +544,7 @@ App::patch('/v1/projects/:projectId/service/all')
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$allServices = array_keys(array_filter(Config::getParam('services'), fn($element) => $element['optional']));
$allServices = array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional']));
$services = [];
foreach ($allServices as $service) {
@@ -730,8 +843,7 @@ App::delete('/v1/projects/:projectId')
$deletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($project)
;
->setDocument($project);
if (!$dbForConsole->deleteDocument('projects', $projectId)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove project from DB');
@@ -909,8 +1021,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->setAttribute('url', $url)
->setAttribute('security', $security)
->setAttribute('httpUser', $httpUser)
->setAttribute('httpPass', $httpPass)
;
->setAttribute('httpPass', $httpPass);
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
@@ -1149,8 +1260,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
$key
->setAttribute('name', $name)
->setAttribute('scopes', $scopes)
->setAttribute('expire', $expire)
;
->setAttribute('expire', $expire);
$dbForConsole->updateDocument('keys', $key->getId(), $key);
@@ -1352,8 +1462,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
->setAttribute('name', $name)
->setAttribute('key', $key)
->setAttribute('store', $store)
->setAttribute('hostname', $hostname)
;
->setAttribute('hostname', $hostname);
$dbForConsole->updateDocument('platforms', $platform->getId(), $platform);
@@ -1399,248 +1508,6 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
$response->noContent();
});
// Domains
App::post('/v1/projects/:projectId/domains')
->desc('Create Domain')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'createDomain')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DOMAIN)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('domain', null, new DomainValidator(), 'Domain name.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $domain, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
if ($domain === App::getEnv('_APP_DOMAIN', '') || $domain === App::getEnv('_APP_DOMAIN_TARGET', '')) {
throw new Exception(Exception::DOMAIN_FORBIDDEN);
}
$document = $dbForConsole->findOne('domains', [
Query::equal('domain', [$domain])
]);
if ($document && !$document->isEmpty()) {
throw new Exception(Exception::DOMAIN_ALREADY_EXISTS);
}
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
if (!$target->isKnown() || $target->isTest()) {
throw new Exception(Exception::DOMAIN_TARGET_INVALID, 'Unreachable CNAME target (' . $target->get() . '). Please check the _APP_DOMAIN_TARGET environment variable of your Appwrite server.');
}
$domain = new Domain($domain);
$domain = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'updated' => DateTime::now(),
'domain' => $domain->get(),
'tld' => $domain->getSuffix(),
'registerable' => $domain->getRegisterable(),
'verification' => false,
'certificateId' => null,
]);
$domain = $dbForConsole->createDocument('domains', $domain);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($domain, Response::MODEL_DOMAIN);
});
App::get('/v1/projects/:projectId/domains')
->desc('List Domains')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'listDomains')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DOMAIN_LIST)
->param('projectId', '', new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$domains = $dbForConsole->find('domains', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(5000),
]);
$response->dynamic(new Document([
'domains' => $domains,
'total' => count($domains),
]), Response::MODEL_DOMAIN_LIST);
});
App::get('/v1/projects/:projectId/domains/:domainId')
->desc('Get Domain')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'getDomain')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DOMAIN)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('domainId', '', new UID(), 'Domain unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $domainId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$domain = $dbForConsole->findOne('domains', [
Query::equal('$id', [$domainId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($domain === false || $domain->isEmpty()) {
throw new Exception(Exception::DOMAIN_NOT_FOUND);
}
$response->dynamic($domain, Response::MODEL_DOMAIN);
});
App::patch('/v1/projects/:projectId/domains/:domainId/verification')
->desc('Update Domain Verification Status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateDomainVerification')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DOMAIN)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('domainId', '', new UID(), 'Domain unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $domainId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$domain = $dbForConsole->findOne('domains', [
Query::equal('$id', [$domainId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($domain === false || $domain->isEmpty()) {
throw new Exception(Exception::DOMAIN_NOT_FOUND);
}
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
if (!$target->isKnown() || $target->isTest()) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unreachable CNAME target (' . $target->get() . '), please use a domain with a public suffix.');
}
if ($domain->getAttribute('verification') === true) {
return $response->dynamic($domain, Response::MODEL_DOMAIN);
}
$validator = new CNAME($target->get()); // Verify Domain with DNS records
if (!$validator->isValid($domain->getAttribute('domain', ''))) {
throw new Exception(Exception::DOMAIN_VERIFICATION_FAILED);
}
$dbForConsole->updateDocument('domains', $domain->getId(), $domain->setAttribute('verification', true));
$dbForConsole->deleteCachedDocument('projects', $project->getId());
// Issue a TLS certificate when domain is verified
$event = new Certificate();
$event
->setDomain($domain)
->trigger();
$response->dynamic($domain, Response::MODEL_DOMAIN);
});
App::delete('/v1/projects/:projectId/domains/:domainId')
->desc('Delete Domain')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'deleteDomain')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('domainId', '', new UID(), 'Domain unique ID.')
->inject('response')
->inject('dbForConsole')
->inject('deletes')
->action(function (string $projectId, string $domainId, Response $response, Database $dbForConsole, Delete $deletes) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$domain = $dbForConsole->findOne('domains', [
Query::equal('$id', [$domainId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($domain === false || $domain->isEmpty()) {
throw new Exception(Exception::DOMAIN_NOT_FOUND);
}
if ($domain->getAttribute('domain') === App::getEnv('_APP_DOMAIN', '') || $domain->getAttribute('domain') === App::getEnv('_APP_DOMAIN_TARGET', '')) {
throw new Exception(Exception::DOMAIN_FORBIDDEN);
}
$dbForConsole->deleteDocument('domains', $domain->getId());
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$deletes
->setType(DELETE_TYPE_CERTIFICATES)
->setDocument($domain);
$response->noContent();
});
// CUSTOM SMTP and Templates
App::patch('/v1/projects/:projectId/smtp')
->desc('Update SMTP configuration')
@@ -1654,15 +1521,17 @@ App::patch('/v1/projects/:projectId/smtp')
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('enabled', false, new Boolean(), 'Enable custom SMTP service')
->param('sender', '', new Email(), 'SMTP sender email')
->param('host', '', new HostName(), 'SMTP server host name')
->param('port', null, new Integer(), 'SMTP server port')
->param('username', null, new Text(0), 'SMTP server username')
->param('password', null, new Text(0), 'SMTP server password')
->param('senderName', '', new Text(255, 0), 'Name of the email sender', true)
->param('senderEmail', '', new Email(), 'Email of the sender', true)
->param('replyTo', '', new Email(), 'Reply to email', true)
->param('host', '', new HostName(), 'SMTP server host name', true)
->param('port', 587, new Integer(), 'SMTP server port', true)
->param('username', '', new Text(0, 0), 'SMTP server username', true)
->param('password', '', new Text(0, 0), 'SMTP server password', true)
->param('secure', '', new WhiteList(['tls'], true), 'Does SMTP server use secure connection', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, bool $enabled, string $sender, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForConsole) {
->action(function (string $projectId, bool $enabled, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -1670,30 +1539,60 @@ App::patch('/v1/projects/:projectId/smtp')
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
// validate SMTP settings
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Username = $username;
$mail->Password = $password;
$mail->Host = $host;
$mail->Port = $port;
$mail->SMTPSecure = $secure;
$mail->SMTPAutoTLS = false;
$valid = $mail->SmtpConnect();
if (!$valid) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED);
// Ensure required params for when enabling SMTP
if ($enabled) {
if (empty($senderName)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Sender name is required when enabling SMTP.');
} elseif (empty($senderEmail)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Sender email is required when enabling SMTP.');
} elseif (empty($host)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Host is required when enabling SMTP.');
} elseif (empty($port)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Port is required when enabling SMTP.');
}
}
$smtp = [
'enabled' => $enabled,
'sender' => $sender,
'host' => $host,
'port' => $port,
'username' => $username,
'password' => $password,
'secure' => $secure,
];
// validate SMTP settings
if ($enabled) {
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Username = $username;
$mail->Password = $password;
$mail->Host = $host;
$mail->Port = $port;
$mail->SMTPSecure = $secure;
$mail->SMTPAutoTLS = false;
$mail->Timeout = 5;
try {
$valid = $mail->SmtpConnect();
if (!$valid) {
throw new Exception('Connection is not valid.');
}
} catch (Throwable $error) {
throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, 'Could not connect to SMTP server: ' . $error->getMessage());
}
}
// Save SMTP settings
if ($enabled) {
$smtp = [
'enabled' => $enabled,
'senderName' => $senderName,
'senderEmail' => $senderEmail,
'replyTo' => $replyTo,
'host' => $host,
'port' => $port,
'username' => $username,
'password' => $password,
'secure' => $secure,
];
} else {
$smtp = [
'enabled' => false
];
}
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('smtp', $smtp));
@@ -1712,11 +1611,13 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale')
->label('sdk.response.model', Response::MODEL_SMS_TEMPLATE)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? []), 'Template type')
->param('locale', '', fn($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@@ -1728,7 +1629,7 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale')
if (is_null($template)) {
$template = [
'message' => Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl')->render(),
'message' => Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl')->render(),
];
}
@@ -1750,7 +1651,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale')
->label('sdk.response.model', Response::MODEL_EMAIL_TEMPLATE)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? []), 'Template type')
->param('locale', '', fn($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
@@ -1766,27 +1667,22 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale')
$localeObj = new Locale($locale);
if (is_null($template)) {
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
$message = $message
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
$message
->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello"))
->setParam('{{name}}', '')
->setParam('{{body}}', $localeObj->getText("emails.{$type}.body"))
->setParam('{{user}}', '')
->setParam('{{footer}}', $localeObj->getText("emails.{$type}.footer"))
->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'))
->setParam('{{thanks}}', $localeObj->getText("emails.{$type}.thanks"))
->setParam('{{signature}}', $localeObj->getText("emails.{$type}.signature"))
->setParam('{{direction}}', $localeObj->getText('settings.direction'))
->setParam('{{bg-body}}', '#f7f7f7')
->setParam('{{bg-content}}', '#ffffff')
->setParam('{{text-content}}', '#000000')
->render();
->setParam('{{direction}}', $localeObj->getText('settings.direction'));
$message = $message->render();
$from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($localeObj->getText('emails.sender'), $project->getAttribute('name'));
$from = empty($from) ? \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')) : $from;
$template = [
'message' => $message,
'subject' => $localeObj->getText('emails.' . $type . '.subject'),
'senderEmail' => App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', ''),
'senderName' => $from
'senderEmail' => '',
'senderName' => ''
];
}
@@ -1808,12 +1704,14 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale')
->label('sdk.response.model', Response::MODEL_SMS_TEMPLATE)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? []), 'Template type')
->param('locale', '', fn($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->param('message', '', new Text(0), 'Template message')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForConsole) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@@ -1846,15 +1744,15 @@ App::patch('/v1/projects/:projectId/templates/email/:type/:locale')
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? []), 'Template type')
->param('locale', '', fn($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->param('senderName', '', new Text(255), 'Name of the email sender')
->param('senderEmail', '', new Email(), 'Email of the sender')
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['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)
->param('senderEmail', '', new Email(), 'Email of the sender', true)
->param('replyTo', '', new Email(), 'Reply to email', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, string $senderName, string $senderEmail, string $subject, string $message, string $replyTo, Response $response, Database $dbForConsole) {
->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -1896,11 +1794,13 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
->label('sdk.response.model', Response::MODEL_SMS_TEMPLATE)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? []), 'Template type')
->param('locale', '', fn($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@@ -1937,7 +1837,7 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale')
->label('sdk.response.model', Response::MODEL_EMAIL_TEMPLATE)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? []), 'Template type')
->param('locale', '', fn($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {