Improve endpoint quality

This commit is contained in:
Matej Bačo
2026-04-20 11:28:21 +02:00
parent 7fe65eec57
commit c4d9c3dc4f
5 changed files with 169 additions and 222 deletions
@@ -1,144 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\SMTP\Credentials;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use PHPMailer\PHPMailer\PHPMailer;
use Throwable;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Emails\Validator\Email;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectSMTPCredentials';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/smtp/credentials')
->httpAlias('/v1/projects/:projectId/smtp')
->desc('Update project SMTP details')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'smtp.*.update')
->label('audits.event', 'project.smtp.update')
->label('audits.resource', 'project.smtp/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'smtp',
name: 'updateSMTPCredentials',
description: <<<EOT
Update the SMTP configuration for your project. Use this endpoint to configure your project's SMTP provider with your custom settings for sending transactional emails.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
))
->param('senderName', '', new Text(256), 'Name of the email sender')
->param('senderEmail', '', new Email(), 'Email of the sender')
->param('replyTo', '', new Email(), 'Reply to email', true)
->param('host', '', new Hostname(), 'SMTP server host name')
->param('port', 587, new Integer(), 'SMTP server port')
->param('username', '', new Text(256), 'SMTP server username', true)
->param('password', '', new Text(256), 'SMTP server password', true)
->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', true)
->param('enabled', null, new Nullable(new Boolean()), 'Enable custom SMTP service', optional: true, deprecated: true) // Backwards compatibility
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $senderName,
string $senderEmail,
string $replyTo,
string $host,
int $port,
string $username,
string $password,
string $secure,
?bool $enabled, // Backwards compatibility
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
// Backwards compatibility
if (!\is_null($enabled) && $enabled === false) {
$smtp = $project->getAttribute('smtp', []);
$smtp['enabled'] = $enabled;
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('smtp', $smtp)));
$response->dynamic($project, Response::MODEL_PROJECT);
return;
}
// Validate SMTP settings
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->SMTPAuth = (!empty($username) && !empty($password));
$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, $error->getMessage());
}
$smtp = [
'enabled' => true,
'senderName' => $senderName,
'senderEmail' => $senderEmail,
'replyTo' => $replyTo,
'host' => $host,
'port' => $port,
'username' => $username,
'password' => $password,
'secure' => $secure,
];
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('smtp', $smtp)));
$response->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -1,74 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\SMTP\Status;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectSMTPStatus';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/smtp/status')
->desc('Update project SMTP status')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'smtp.*.update')
->label('audits.event', 'project.smtp.update')
->label('audits.resource', 'project.smtp/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'smtp',
name: 'updateSMTPStatus',
description: <<<EOT
Update the status of a SMTP. Use this endpoint to enable or disable ability to configure custom email sender in your project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
))
->param('enabled', null, new Boolean(), 'SMTP status.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
}
public function action(
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
$smtp = $project->getAttribute('smtp', []);
$smtp['enabled'] = $enabled;
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('smtp', $smtp)));
$response->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -0,0 +1,157 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\SMTP;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use PHPMailer\PHPMailer\PHPMailer;
use Throwable;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Emails\Validator\Email;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectSMTP';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/smtp')
->httpAlias('/v1/projects/:projectId/smtp')
->desc('Update project SMTP configuration')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'smtp.*.update')
->label('audits.event', 'project.smtp.update')
->label('audits.resource', 'project.smtp/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'smtp',
name: 'updateSMTP',
description: <<<EOT
Update the SMTP configuration for your project. Use this endpoint to configure your project's SMTP provider with your custom settings for sending transactional emails.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
))
->param('host', '', new Nullable(new Hostname()), 'SMTP server hostname (domain)', optional: true)
->param('port', 587, new Nullable(new Integer()), 'SMTP server port', optional: true)
->param('username', '', new Nullable(new Text(256)), 'SMTP server username. Leave empty for no authorization.', optional: true)
->param('password', '', new Nullable(new Text(256)), 'SMTP server password. Leave empty for no authorization. This property is stored securely and cannot be read in future (write-only).', optional: true)
->param('senderEmail', '', new Nullable(new Email()), 'Email address shown in inbox as the sender of the email.', optional: true)
->param('senderName', '', new Nullable(new Text(256)), 'Name shown in inbox as the sender of the email.', optional: true)
->param('replyToEmail', '', new Nullable(new Email()), 'Email used when user replies to the email.', optional: true)
->param('replyToName', '', new Nullable(new Text(256)), 'Name used when user replies to the email.', optional: true)
->param('secure', '', new Nullable(new WhiteList(['tls', 'ssl'], true)), 'Configures if communication with SMTP server is encrypted. Allowed values are: tls, ssl. Leave empty for no encryption.', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'Enable or disable custom SMTP. Custom SMTP is useful for branding purposes, but also allows use of custom email templates.', optional: true)
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
}
public function action(
?string $host,
?int $port,
?string $username,
?string $password,
?string $senderEmail,
?string $senderName,
?string $replyToEmail,
?string $replyToName,
?string $secure,
?bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
// Fetch current configuration
$smtp = $project->getAttribute('smtp', []);
// Apply changes
$keys = ['host', 'port', 'username', 'password', 'senderEmail', 'senderName', 'replyToEmail', 'replyToName', 'secure', 'enabled'];
foreach ($keys as $key) {
if (!\is_null(${$key})) {
$smtp[$key] = ${$key};
}
}
// Ensure required fields are set
$requiredKeys = ['host', 'port', 'senderEmail'];
foreach ($requiredKeys as $key) {
if (empty($smtp[$key])) {
throw new \Exception('"' . $key . '" is required. Please provide a value.');
}
}
// Validate SMTP credentials
if($smtp['enabled'] === true) {
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = $smtp['host'] ?? '';
$mail->Port = $smtp['port'] ?? '';
$mail->SMTPSecure = $smtp['secure'] ?? '';
$mail->setFrom($smtp['senderEmail'], $smtp['senderName'] ?? '');
if(!empty($smtp['username'] ?? '')) {
$mail->SMTPAuth = true;
$mail->Username = $smtp['username'];
$mail->Password = $smtp['password'] ?? '';
}
if(!empty($smtp['replyToEmail'] ?? '')) {
$mail->addReplyTo($smtp['replyToEmail'], $smtp['replyToName'] ?? '');
}
$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, $error->getMessage());
}
}
// Save configuration
$updates = new Document([
'smtp' => $smtp,
]);
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
$response->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -25,7 +25,6 @@ use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatfo
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\SMTP\Credentials\Update as UpdateSMTPCredentials;
use Appwrite\Platform\Modules\Project\Http\Project\SMTP\Status\Update as UpdateSMTPStatus;
use Appwrite\Platform\Modules\Project\Http\Project\SMTP\Tests\Create as CreateSMTPTest;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
@@ -50,7 +49,6 @@ class Http extends Service
// SMTP
$this->addAction(UpdateSMTPCredentials::getName(), new UpdateSMTPCredentials());
$this->addAction(UpdateSMTPStatus::getName(), new UpdateSMTPStatus());
$this->addAction(CreateSMTPTest::getName(), new CreateSMTPTest());
// Variables
+12 -2
View File
@@ -247,7 +247,13 @@ class Project extends Model
'default' => '',
'example' => 'john@appwrite.io',
])
->addRule('smtpReplyTo', [
->addRule('smtpReplyToName', [
'type' => self::TYPE_STRING,
'description' => 'SMTP reply to name',
'default' => '',
'example' => 'Support Team',
])
->addRule('smtpReplyToEmail', [
'type' => self::TYPE_STRING,
'description' => 'SMTP reply to email',
'default' => '',
@@ -271,12 +277,15 @@ class Project extends Model
'default' => '',
'example' => 'emailuser',
])
/*
We intentionally do not expose SMTP password - it's write-only property.
->addRule('smtpPassword', [
'type' => self::TYPE_STRING,
'description' => 'SMTP server password',
'default' => '',
'example' => 'securepassword',
])
*/
->addRule('smtpSecure', [
'type' => self::TYPE_STRING,
'description' => 'SMTP server secure protocol',
@@ -409,7 +418,8 @@ class Project extends Model
$document->setAttribute('smtpEnabled', $smtp['enabled'] ?? false);
$document->setAttribute('smtpSenderEmail', $smtp['senderEmail'] ?? '');
$document->setAttribute('smtpSenderName', $smtp['senderName'] ?? '');
$document->setAttribute('smtpReplyTo', $smtp['replyTo'] ?? '');
$document->setAttribute('smtpReplyToEmail', $smtp['replyToEmail'] ?? '');
$document->setAttribute('smtpReplyToName', $smtp['replyToName'] ?? '');
$document->setAttribute('smtpHost', $smtp['host'] ?? '');
$document->setAttribute('smtpPort', $smtp['port'] ?? '');
$document->setAttribute('smtpUsername', $smtp['username'] ?? '');