mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 08:58:35 +00:00
Merge branch 'update-sdks-1.7.x' into add-docs-swift-apple
This commit is contained in:
commit
3c6291024b
37 changed files with 519 additions and 192 deletions
|
|
@ -9,7 +9,31 @@ You can [find issues using this query](https://github.com/search?q=org%3Aappwrit
|
|||
|
||||
## How to Start?
|
||||
|
||||
If you are worried or don’t know where to start, check out the next section that explains what kind of help we could use and where you can get involved. You can send your questions to [@appwrite on Twitter](https://twitter.com/appwrite) or to anyone from the [Appwrite team on Discord](https://appwrite.io/discord). You can also submit an issue, and a maintainer can guide you!
|
||||
Welcome! We're excited that you're interested in contributing to Appwrite. To make sure your time is valued and your contributions are successful, please follow these steps before writing any code:
|
||||
|
||||
### 🔍 Step 1: Find an Issue
|
||||
|
||||
Browse open issues and look for ones labeled [good first issue](https://github.com/search?q=org%3Aappwrite+is%3Aopen+type%3Aissue+label%3A%22good+first+issue%22&type=issues) or [help wanted](https://github.com/search?q=org%3Aappwrite+is%3Aopen+type%3Aissue+label%3A%22help+wanted%22&type=issues).
|
||||
|
||||
If you're not sure which issue to pick, ask in our [maintainers channel](https://discord.com/channels/564160730845151244/636852860709240842) on Discord.
|
||||
|
||||
### 📝 Step 2: Ask to Be Assigned
|
||||
|
||||
Before working on an issue, comment on the GitHub issue asking to be assigned. This prevents multiple people working on the same task.
|
||||
|
||||
Then, create a thread in the [maintainers channel](https://discord.com/channels/564160730845151244/636852860709240842) on Discord with a link to the issue.
|
||||
|
||||
Our team is small and may not see your GitHub comment right away - posting in the [maintainers channel](https://discord.com/channels/564160730845151244/636852860709240842) ensures it gets seen.
|
||||
|
||||
### 💬 Step 3: Don’t Submit Random PRs
|
||||
|
||||
If you're not working on an assigned issue, create a GitHub issue first.
|
||||
|
||||
PRs submitted without context or discussion may not align with our roadmap and may be closed without review.
|
||||
|
||||
### ⚠️ Please Note
|
||||
|
||||
We’re a very small team managing a large project. Many PRs are submitted, and while we appreciate every effort, we can only review contributions that follow the process above. This helps us keep things fair and organized.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
|
|
|
|||
12
app/cli.php
12
app/cli.php
|
|
@ -191,9 +191,15 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
|
|||
CLI::setResource('publisher', function (Group $pools) {
|
||||
return new BrokerPool(publisher: $pools->get('publisher'));
|
||||
}, ['pools']);
|
||||
CLI::setResource('publisherRedis', function () {
|
||||
// Stub
|
||||
});
|
||||
CLI::setResource('publisherDatabases', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
CLI::setResource('publisherMigrations', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
CLI::setResource('publisherStatsUsage', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
CLI::setResource('queueForStatsUsage', function (Publisher $publisher) {
|
||||
return new StatsUsage($publisher);
|
||||
}, ['publisher']);
|
||||
|
|
|
|||
|
|
@ -120,6 +120,11 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<div style="display: none; overflow: hidden; max-height: 0; max-width: 0; opacity: 0; line-height: 1px;">
|
||||
{{preview}}
|
||||
<div>{{previewWhitespace}}</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<table>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -121,6 +121,11 @@
|
|||
|
||||
<body style="direction: {{direction}}">
|
||||
|
||||
<div style="display: none; overflow: hidden; max-height: 0; max-width: 0; opacity: 0; line-height: 1px;">
|
||||
{{preview}}
|
||||
<div>{{previewWhitespace}}</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width:650px; word-wrap: break-word; overflow-wrap: break-word;
|
||||
word-break: normal; margin:0 auto;">
|
||||
<table style="margin-top: 32px">
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@
|
|||
<p>If you have trouble with the sender's image, ensure it is set in the <a href="https://gravatar.com/">Gravatar database</a>.</p>
|
||||
|
||||
<p style="margin-block-end: 0;">Best regards,</p>
|
||||
<p style="margin-block-start: 0;">Appwrtite team</p>
|
||||
<p style="margin-block-start: 0;">Appwrite team</p>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
<table border="0" cellspacing="0" cellpadding="0" style="padding-top: 10px; padding-bottom: 10px; margin-top: 32px">
|
||||
<tr>
|
||||
<td style="border-radius: 8px; display: block; width: 100%;">
|
||||
<a class="mobile-full-width" rel="noopener" target="_blank" href="{{host}}{{path}}" style="font-size: 14px; font-family: Inter; color: #ffffff; text-decoration: none; background-color: #FD366E; border-radius: 8px; padding: 9px 14px; border: 1px solid #FD366E; display: inline-block; text-align:center; box-sizing: border-box;">Webhook settings</a>
|
||||
<a class="mobile-full-width" rel="noopener" target="_blank" href="{{host}}{{path}}" style="font-size: 14px; font-family: Inter; color: #ffffff; text-decoration: none; background-color: #2D2D31; border-radius: 8px; padding: 9px 14px; border: 1px solid #414146; display: inline-block; text-align:center; box-sizing: border-box;">Webhook settings</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
"settings.direction": "ltr",
|
||||
"emails.sender": "%s Team",
|
||||
"emails.verification.subject": "Account Verification",
|
||||
"emails.verification.preview": "Verify your email to activate your {{project}} account.",
|
||||
"emails.verification.hello": "Hello {{user}},",
|
||||
"emails.verification.body": "Follow this link to verify your email address to your {{b}}{{project}}{{/b}} account.",
|
||||
"emails.verification.footer": "If you didn’t ask to verify this address, you can ignore this message.",
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
"emails.verification.buttonText": "Confirm email address",
|
||||
"emails.verification.signature": "{{project}} team",
|
||||
"emails.magicSession.subject": "{{project}} Login",
|
||||
"emails.magicSession.preview": "Sign in to {{project}} with your secure link. Expires in 1 hour.",
|
||||
"emails.magicSession.hello": "Hello {{user}},",
|
||||
"emails.magicSession.optionButton": "Click the button below to securely sign in to your {{b}}{{project}}{{/b}} account. This link will expire in 1 hour.",
|
||||
"emails.magicSession.buttonText": "Sign in to {{project}}",
|
||||
|
|
@ -20,6 +22,7 @@
|
|||
"emails.magicSession.thanks": "Thanks,",
|
||||
"emails.magicSession.signature": "{{project}} team",
|
||||
"emails.sessionAlert.subject": "Security alert: new session on your {{project}} account",
|
||||
"emails.sessionAlert.preview": "New login detected on {{project}} at {{time}} UTC.",
|
||||
"emails.sessionAlert.hello": "Hello {{user}},",
|
||||
"emails.sessionAlert.body": "A new session has been created on your {{b}}{{project}}{{/b}} account, {{b}}on {{date}}, {{year}} at {{time}} UTC{{/b}}.\nHere are the details of the new session: ",
|
||||
"emails.sessionAlert.listDevice": "Device: {{b}}{{device}}{{/b}}",
|
||||
|
|
@ -29,6 +32,7 @@
|
|||
"emails.sessionAlert.thanks": "Thanks,",
|
||||
"emails.sessionAlert.signature": "{{project}} team",
|
||||
"emails.otpSession.subject": "OTP for {{project}} Login",
|
||||
"emails.otpSession.preview": "Use OTP {{otp}} to sign in to {{project}}. Expires in 15 minutes.",
|
||||
"emails.otpSession.hello": "Hello {{user}},",
|
||||
"emails.otpSession.description": "Enter the following verification code when prompted to securely sign in to your {{b}}{{project}}{{/b}} account. This code will expire in 15 minutes.",
|
||||
"emails.otpSession.clientInfo": "This sign in was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the sign in, you can safely ignore this email.",
|
||||
|
|
@ -36,12 +40,14 @@
|
|||
"emails.otpSession.thanks": "Thanks,",
|
||||
"emails.otpSession.signature": "{{project}} team",
|
||||
"emails.mfaChallenge.subject": "Verification Code for {{project}}",
|
||||
"emails.mfaChallenge.preview": "Use code {{otp}} for two-step verification in {{project}}. Expires in 15 minutes.",
|
||||
"emails.mfaChallenge.hello": "Hello {{user}},",
|
||||
"emails.mfaChallenge.description": "Enter the following verification code to verify your email and activate two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.",
|
||||
"emails.mfaChallenge.description": "Enter the following code to confirm your two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.",
|
||||
"emails.mfaChallenge.clientInfo": "This verification code was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the verification code, you can safely ignore this email.",
|
||||
"emails.mfaChallenge.thanks": "Thanks,",
|
||||
"emails.mfaChallenge.signature": "{{project}} team",
|
||||
"emails.recovery.subject": "Password Reset",
|
||||
"emails.recovery.preview": "Reset your {{project}} password using the link.",
|
||||
"emails.recovery.hello": "Hello {{user}},",
|
||||
"emails.recovery.body": "Follow this link to reset your {{b}}{{project}}{{/b}} password.",
|
||||
"emails.recovery.footer": "If you didn't ask to reset your password, you can ignore this message.",
|
||||
|
|
@ -49,6 +55,7 @@
|
|||
"emails.recovery.buttonText": "Reset password",
|
||||
"emails.recovery.signature": "{{project}} team",
|
||||
"emails.invitation.subject": "Invitation to %s Team at %s",
|
||||
"emails.invitation.preview": "{{owner}} invited you to join {{team}} at {{project}}",
|
||||
"emails.invitation.hello": "Hello {{user}},",
|
||||
"emails.invitation.body": "This mail was sent to you because {{b}}{{owner}}{{/b}} wanted to invite you to become a member of the {{b}}{{team}}{{/b}} team at {{b}}{{project}}{{/b}}.",
|
||||
"emails.invitation.footer": "If you are not interested, you can ignore this message.",
|
||||
|
|
@ -56,6 +63,7 @@
|
|||
"emails.invitation.buttonText": "Accept invite to {{team}}",
|
||||
"emails.invitation.signature": "{{project}} team",
|
||||
"emails.certificate.subject": "Certificate failure for %s",
|
||||
"emails.certificate.preview": "Your domain %s certificate generation has failed.",
|
||||
"emails.certificate.hello": "Hello,",
|
||||
"emails.certificate.body": "Certificate for your domain '{{domain}}' could not be generated. This is attempt no. {{attempt}}, and the failure was caused by: {{error}}",
|
||||
"emails.certificate.footer": "Your previous certificate will be valid for 30 days since the first failure. We highly recommend investigating this case, otherwise your domain will end up without a valid SSL communication.",
|
||||
|
|
|
|||
|
|
@ -4489,6 +4489,30 @@
|
|||
}
|
||||
],
|
||||
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console."
|
||||
},
|
||||
{
|
||||
"name": "createDocuments",
|
||||
"auth": {
|
||||
"Admin": [],
|
||||
"Key": []
|
||||
},
|
||||
"parameters": [
|
||||
"databaseId",
|
||||
"collectionId",
|
||||
"documents"
|
||||
],
|
||||
"required": [
|
||||
"databaseId",
|
||||
"collectionId",
|
||||
"documents"
|
||||
],
|
||||
"responses": [
|
||||
{
|
||||
"code": 201,
|
||||
"model": "#\/components\/schemas\/documentList"
|
||||
}
|
||||
],
|
||||
"description": "**WARNING: Experimental Feature** - This endpoint is experimental and not yet officially supported. It may be subject to breaking changes or removal in future versions.\n\nCreate new Documents. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console."
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
|
|
|
|||
|
|
@ -39698,6 +39698,11 @@
|
|||
"description": "Is VCS (Version Control System) repository private?",
|
||||
"x-example": true
|
||||
},
|
||||
"defaultBranch": {
|
||||
"type": "string",
|
||||
"description": "VCS (Version Control System) repository's default branch name.",
|
||||
"x-example": "main"
|
||||
},
|
||||
"pushedAt": {
|
||||
"type": "string",
|
||||
"description": "Last commit date in ISO 8601 format.",
|
||||
|
|
@ -39710,6 +39715,7 @@
|
|||
"organization",
|
||||
"provider",
|
||||
"private",
|
||||
"defaultBranch",
|
||||
"pushedAt"
|
||||
]
|
||||
},
|
||||
|
|
@ -39742,6 +39748,11 @@
|
|||
"description": "Is VCS (Version Control System) repository private?",
|
||||
"x-example": true
|
||||
},
|
||||
"defaultBranch": {
|
||||
"type": "string",
|
||||
"description": "VCS (Version Control System) repository's default branch name.",
|
||||
"x-example": "main"
|
||||
},
|
||||
"pushedAt": {
|
||||
"type": "string",
|
||||
"description": "Last commit date in ISO 8601 format.",
|
||||
|
|
@ -39759,6 +39770,7 @@
|
|||
"organization",
|
||||
"provider",
|
||||
"private",
|
||||
"defaultBranch",
|
||||
"pushedAt",
|
||||
"framework"
|
||||
]
|
||||
|
|
@ -39792,6 +39804,11 @@
|
|||
"description": "Is VCS (Version Control System) repository private?",
|
||||
"x-example": true
|
||||
},
|
||||
"defaultBranch": {
|
||||
"type": "string",
|
||||
"description": "VCS (Version Control System) repository's default branch name.",
|
||||
"x-example": "main"
|
||||
},
|
||||
"pushedAt": {
|
||||
"type": "string",
|
||||
"description": "Last commit date in ISO 8601 format.",
|
||||
|
|
@ -39809,6 +39826,7 @@
|
|||
"organization",
|
||||
"provider",
|
||||
"private",
|
||||
"defaultBranch",
|
||||
"pushedAt",
|
||||
"runtime"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4626,6 +4626,30 @@
|
|||
}
|
||||
],
|
||||
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console."
|
||||
},
|
||||
{
|
||||
"name": "createDocuments",
|
||||
"auth": {
|
||||
"Admin": [],
|
||||
"Key": []
|
||||
},
|
||||
"parameters": [
|
||||
"databaseId",
|
||||
"collectionId",
|
||||
"documents"
|
||||
],
|
||||
"required": [
|
||||
"databaseId",
|
||||
"collectionId",
|
||||
"documents"
|
||||
],
|
||||
"responses": [
|
||||
{
|
||||
"code": 201,
|
||||
"model": "#\/definitions\/documentList"
|
||||
}
|
||||
],
|
||||
"description": "**WARNING: Experimental Feature** - This endpoint is experimental and not yet officially supported. It may be subject to breaking changes or removal in future versions.\n\nCreate new Documents. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console."
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
|
|
|
|||
|
|
@ -39874,6 +39874,11 @@
|
|||
"description": "Is VCS (Version Control System) repository private?",
|
||||
"x-example": true
|
||||
},
|
||||
"defaultBranch": {
|
||||
"type": "string",
|
||||
"description": "VCS (Version Control System) repository's default branch name.",
|
||||
"x-example": "main"
|
||||
},
|
||||
"pushedAt": {
|
||||
"type": "string",
|
||||
"description": "Last commit date in ISO 8601 format.",
|
||||
|
|
@ -39886,6 +39891,7 @@
|
|||
"organization",
|
||||
"provider",
|
||||
"private",
|
||||
"defaultBranch",
|
||||
"pushedAt"
|
||||
]
|
||||
},
|
||||
|
|
@ -39918,6 +39924,11 @@
|
|||
"description": "Is VCS (Version Control System) repository private?",
|
||||
"x-example": true
|
||||
},
|
||||
"defaultBranch": {
|
||||
"type": "string",
|
||||
"description": "VCS (Version Control System) repository's default branch name.",
|
||||
"x-example": "main"
|
||||
},
|
||||
"pushedAt": {
|
||||
"type": "string",
|
||||
"description": "Last commit date in ISO 8601 format.",
|
||||
|
|
@ -39935,6 +39946,7 @@
|
|||
"organization",
|
||||
"provider",
|
||||
"private",
|
||||
"defaultBranch",
|
||||
"pushedAt",
|
||||
"framework"
|
||||
]
|
||||
|
|
@ -39968,6 +39980,11 @@
|
|||
"description": "Is VCS (Version Control System) repository private?",
|
||||
"x-example": true
|
||||
},
|
||||
"defaultBranch": {
|
||||
"type": "string",
|
||||
"description": "VCS (Version Control System) repository's default branch name.",
|
||||
"x-example": "main"
|
||||
},
|
||||
"pushedAt": {
|
||||
"type": "string",
|
||||
"description": "Last commit date in ISO 8601 format.",
|
||||
|
|
@ -39985,6 +40002,7 @@
|
|||
"organization",
|
||||
"provider",
|
||||
"private",
|
||||
"defaultBranch",
|
||||
"pushedAt",
|
||||
"runtime"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ $oauthDefaultFailure = '/console/auth/oauth2/failure';
|
|||
function sendSessionAlert(Locale $locale, Document $user, Document $project, Document $session, Mail $queueForMails)
|
||||
{
|
||||
$subject = $locale->getText("emails.sessionAlert.subject");
|
||||
$preview = $locale->getText("emails.sessionAlert.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.sessionAlert-' . $locale->default] ?? [];
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-session-alert.tpl');
|
||||
|
|
@ -132,6 +133,16 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, Doc
|
|||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
|
||||
// session alerts should always have a client name!
|
||||
$clientName = $session->getAttribute('clientName');
|
||||
if (empty($clientName)) {
|
||||
// fallback to the user agent and then unknown!
|
||||
$userAgent = $session->getAttribute('userAgent');
|
||||
$clientName = !empty($userAgent) ? $userAgent : 'UNKNOWN';
|
||||
|
||||
$session->setAttribute('clientName', $clientName);
|
||||
}
|
||||
|
||||
$emailVariables = [
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
'date' => (new \DateTime())->format('F j'),
|
||||
|
|
@ -148,6 +159,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, Doc
|
|||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setRecipient($email)
|
||||
|
|
@ -2025,6 +2037,7 @@ App::post('/v1/account/tokens/magic-url')
|
|||
$url = Template::unParseURL($url);
|
||||
|
||||
$subject = $locale->getText("emails.magicSession.subject");
|
||||
$preview = $locale->getText("emails.magicSession.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ?? [];
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
|
|
@ -2113,6 +2126,7 @@ App::post('/v1/account/tokens/magic-url')
|
|||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setRecipient($email)
|
||||
|
|
@ -2254,6 +2268,7 @@ App::post('/v1/account/tokens/email')
|
|||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$subject = $locale->getText("emails.otpSession.subject");
|
||||
$preview = $locale->getText("emails.otpSession.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? [];
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
|
|
@ -2339,6 +2354,7 @@ App::post('/v1/account/tokens/email')
|
|||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setRecipient($email)
|
||||
|
|
@ -3265,6 +3281,7 @@ App::post('/v1/account/recovery')
|
|||
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
|
||||
$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] ?? [];
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
|
|
@ -3339,6 +3356,7 @@ App::post('/v1/account/recovery')
|
|||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->trigger();
|
||||
|
||||
$recovery->setAttribute('secret', $secret);
|
||||
|
|
@ -3520,6 +3538,7 @@ App::post('/v1/account/verification')
|
|||
|
||||
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
|
||||
$body = $locale->getText("emails.verification.body");
|
||||
$preview = $locale->getText("emails.verification.preview");
|
||||
$subject = $locale->getText("emails.verification.subject");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? [];
|
||||
|
||||
|
|
@ -3592,6 +3611,7 @@ App::post('/v1/account/verification')
|
|||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setRecipient($user->getAttribute('email'))
|
||||
|
|
@ -4437,6 +4457,7 @@ App::post('/v1/account/mfa/challenge')
|
|||
}
|
||||
|
||||
$subject = $locale->getText("emails.mfaChallenge.subject");
|
||||
$preview = $locale->getText("emails.mfaChallenge.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? [];
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
|
|
@ -4513,6 +4534,7 @@ App::post('/v1/account/mfa/challenge')
|
|||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setRecipient($user->getAttribute('email'))
|
||||
|
|
|
|||
|
|
@ -522,17 +522,11 @@ App::get('/v1/health/queue/databases')
|
|||
))
|
||||
->param('name', 'database_db_main', new Text(256), 'Queue name for which to check the queue size', true)
|
||||
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
|
||||
->inject('publisher')
|
||||
->inject('publisherRedis')
|
||||
->inject('publisherDatabases')
|
||||
->inject('response')
|
||||
->action(function (string $name, int|string $threshold, Publisher $publisher, ?Publisher $publisherRedis, Response $response) {
|
||||
->action(function (string $name, int|string $threshold, Publisher $publisherDatabases, Response $response) {
|
||||
$threshold = \intval($threshold);
|
||||
|
||||
$isRedisFallback = \str_contains(System::getEnv('_APP_WORKER_REDIS_FALLBACK', ''), 'databases');
|
||||
|
||||
$size = $isRedisFallback
|
||||
? $publisherRedis->getQueueSize(new Queue($name))
|
||||
: $publisher->getQueueSize(new Queue($name));
|
||||
$size = $publisherDatabases->getQueueSize(new Queue($name));
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
|
|
@ -659,17 +653,12 @@ App::get('/v1/health/queue/migrations')
|
|||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
|
||||
->inject('publisher')
|
||||
->inject('publisherRedis')
|
||||
->inject('publisherMigrations')
|
||||
->inject('response')
|
||||
->action(function (int|string $threshold, Publisher $publisher, ?Publisher $publisherRedis, Response $response) {
|
||||
->action(function (int|string $threshold, Publisher $publisherMigrations, Response $response) {
|
||||
$threshold = \intval($threshold);
|
||||
|
||||
$isRedisFallback = \str_contains(System::getEnv('_APP_WORKER_REDIS_FALLBACK', ''), 'migrations');
|
||||
|
||||
$size = $isRedisFallback
|
||||
? $publisherRedis->getQueueSize(new Queue(Event::MIGRATIONS_QUEUE_NAME))
|
||||
: $publisher->getQueueSize(new Queue(Event::MIGRATIONS_QUEUE_NAME));
|
||||
$size = $publisherMigrations->getQueueSize(new Queue(Event::MIGRATIONS_QUEUE_NAME));
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
|
|
|
|||
|
|
@ -2175,7 +2175,7 @@ App::post('/v1/projects/:projectId/smtp/tests')
|
|||
->setSmtpSenderName($senderName)
|
||||
->setRecipient($email)
|
||||
->setName('')
|
||||
->setbodyTemplate(__DIR__ . '/../../config/locale/templates/email-base-styled.tpl')
|
||||
->setBodyTemplate(__DIR__ . '/../../config/locale/templates/email-base-styled.tpl')
|
||||
->setBody($template->render())
|
||||
->setVariables([])
|
||||
->setSubject($subject)
|
||||
|
|
@ -2268,16 +2268,53 @@ 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-inner-base.tpl');
|
||||
/**
|
||||
* different templates, different placeholders.
|
||||
*/
|
||||
$templateConfigs = [
|
||||
'magicSession' => [
|
||||
'file' => 'email-magic-url.tpl',
|
||||
'placeholders' => ['optionButton', 'buttonText', 'optionUrl', 'clientInfo', 'securityPhrase']
|
||||
],
|
||||
'mfaChallenge' => [
|
||||
'file' => 'email-mfa-challenge.tpl',
|
||||
'placeholders' => ['description', 'clientInfo']
|
||||
],
|
||||
'otpSession' => [
|
||||
'file' => 'email-otp.tpl',
|
||||
'placeholders' => ['description', 'clientInfo', 'securityPhrase']
|
||||
],
|
||||
'sessionAlert' => [
|
||||
'file' => 'email-session-alert.tpl',
|
||||
'placeholders' => ['body', 'listDevice', 'listIpAddress', 'listCountry', 'footer']
|
||||
],
|
||||
];
|
||||
|
||||
// fallback to the base template.
|
||||
$config = $templateConfigs[$type] ?? [
|
||||
'file' => 'email-inner-base.tpl',
|
||||
'placeholders' => ['buttonText', 'body', 'footer']
|
||||
];
|
||||
|
||||
$templateString = file_get_contents(__DIR__ . '/../../config/locale/templates/' . $config['file']);
|
||||
|
||||
// We use `fromString` due to the replace above
|
||||
$message = Template::fromString($templateString);
|
||||
|
||||
// Set type-specific parameters
|
||||
foreach ($config['placeholders'] as $param) {
|
||||
$escapeHtml = !in_array($param, ['clientInfo', 'body', 'footer', 'description']);
|
||||
$message->setParam("{{{$param}}}", $localeObj->getText("emails.{$type}.{$param}"), escapeHtml: $escapeHtml);
|
||||
}
|
||||
|
||||
$message
|
||||
// common placeholders on all the templates
|
||||
->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello"))
|
||||
->setParam('{{footer}}', $localeObj->getText("emails.{$type}.footer"))
|
||||
->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'), escapeHtml: false)
|
||||
->setParam('{{thanks}}', $localeObj->getText("emails.{$type}.thanks"))
|
||||
->setParam('{{buttonText}}', $localeObj->getText("emails.{$type}.buttonText"))
|
||||
->setParam('{{signature}}', $localeObj->getText("emails.{$type}.signature"))
|
||||
->setParam('{{direction}}', $localeObj->getText('settings.direction'));
|
||||
$message = $message->render();
|
||||
->setParam('{{signature}}', $localeObj->getText("emails.{$type}.signature"));
|
||||
|
||||
// `useContent: false` will strip new lines!
|
||||
$message = $message->render(useContent: true);
|
||||
|
||||
$template = [
|
||||
'message' => $message,
|
||||
|
|
|
|||
|
|
@ -1870,7 +1870,7 @@ App::get('/v1/storage/usage')
|
|||
$total = [];
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
|
@ -1899,7 +1899,7 @@ App::get('/v1/storage/usage')
|
|||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
|
|
@ -1917,8 +1917,8 @@ App::get('/v1/storage/usage')
|
|||
'filesTotal' => $usage[$metrics[1]]['total'],
|
||||
'filesStorageTotal' => $usage[$metrics[2]]['total'],
|
||||
'buckets' => $usage[$metrics[0]]['data'],
|
||||
'files' => $usage[$metrics[1]]['data'],
|
||||
'storage' => $usage[$metrics[2]]['data'],
|
||||
'files' => $usage[$metrics[1]]['data'],
|
||||
'storage' => $usage[$metrics[2]]['data'],
|
||||
]), Response::MODEL_USAGE_STORAGE);
|
||||
});
|
||||
|
||||
|
|
@ -1970,7 +1970,7 @@ App::get('/v1/storage/:bucketId/usage')
|
|||
? $dbForLogs
|
||||
: $dbForProject;
|
||||
|
||||
$result = $db->findOne('stats', [
|
||||
$result = $db->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
|
@ -2000,7 +2000,7 @@ App::get('/v1/storage/:bucketId/usage')
|
|||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
|
|
|
|||
|
|
@ -657,6 +657,7 @@ App::post('/v1/teams/:teamId/memberships')
|
|||
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
|
||||
|
||||
$body = $locale->getText("emails.invitation.body");
|
||||
$preview = $locale->getText("emails.invitation.preview");
|
||||
$subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName);
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? [];
|
||||
|
||||
|
|
@ -729,6 +730,7 @@ App::post('/v1/teams/:teamId/memberships')
|
|||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setBody($body)
|
||||
->setPreview($preview)
|
||||
->setRecipient($invitee->getAttribute('email'))
|
||||
->setName($invitee->getAttribute('name', ''))
|
||||
->setVariables($emailVariables)
|
||||
|
|
|
|||
|
|
@ -1137,6 +1137,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
|
|||
$repository['pushedAt'] = $repository['pushed_at'] ?? '';
|
||||
$repository['organization'] = $installation->getAttribute('organization', '');
|
||||
$repository['provider'] = $installation->getAttribute('provider', '');
|
||||
$repository['defaultBranch'] = $repository['default_branch'] ?? '';
|
||||
|
||||
$response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY);
|
||||
});
|
||||
|
|
@ -1234,7 +1235,8 @@ App::post('/v1/vcs/github/events')
|
|||
$providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? '';
|
||||
$providerCommitHash = $parsedPayload["commitHash"] ?? '';
|
||||
$providerRepositoryOwner = $parsedPayload["owner"] ?? '';
|
||||
$providerCommitAuthor = $parsedPayload["headCommitAuthor"] ?? '';
|
||||
$providerCommitAuthorName = $parsedPayload["headCommitAuthorName"] ?? '';
|
||||
$providerCommitAuthorEmail = $parsedPayload["headCommitAuthorEmail"] ?? '';
|
||||
$providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? '';
|
||||
$providerCommitMessage = $parsedPayload["headCommitMessage"] ?? '';
|
||||
$providerCommitUrl = $parsedPayload["headCommitUrl"] ?? '';
|
||||
|
|
@ -1247,9 +1249,9 @@ App::post('/v1/vcs/github/events')
|
|||
Query::limit(100),
|
||||
]));
|
||||
|
||||
// create new deployment only on push and not when branch is created or deleted
|
||||
if (!$providerBranchCreated && !$providerBranchDeleted) {
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
|
||||
// create new deployment only on push (not committed by us) and not when branch is created or deleted
|
||||
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) {
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
|
||||
}
|
||||
} elseif ($event == $github::EVENT_INSTALLATION) {
|
||||
if ($parsedPayload["action"] == "deleted") {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ const APP_COMPUTE_SPECIFICATION_DEFAULT = Specification::S_1VCPU_512MB;
|
|||
const APP_PLATFORM_SERVER = 'server';
|
||||
const APP_PLATFORM_CLIENT = 'client';
|
||||
const APP_PLATFORM_CONSOLE = 'console';
|
||||
const APP_VCS_GITHUB_USERNAME = 'Appwrite';
|
||||
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
|
||||
|
||||
// Database Reconnect
|
||||
const DATABASE_RECONNECT_SLEEP = 2;
|
||||
|
|
|
|||
|
|
@ -80,15 +80,27 @@ App::setResource('localeCodes', function () {
|
|||
App::setResource('publisher', function (Group $pools) {
|
||||
return new BrokerPool(publisher: $pools->get('publisher'));
|
||||
}, ['pools']);
|
||||
App::setResource('publisherRedis', function () {
|
||||
// Stub
|
||||
});
|
||||
App::setResource('publisherDatabases', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
App::setResource('publisherMigrations', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
App::setResource('publisherStatsUsage', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
App::setResource('consumer', function (Group $pools) {
|
||||
return new BrokerPool(consumer: $pools->get('consumer'));
|
||||
}, ['pools']);
|
||||
App::setResource('consumerRedis', function () {
|
||||
// Stub
|
||||
});
|
||||
App::setResource('consumerDatabases', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
App::setResource('consumerMigrations', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['publisher']);
|
||||
App::setResource('consumerStatsUsage', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['publisher']);
|
||||
App::setResource('queueForMessaging', function (Publisher $publisher) {
|
||||
return new Messaging($publisher);
|
||||
}, ['publisher']);
|
||||
|
|
|
|||
|
|
@ -247,17 +247,33 @@ Server::setResource('publisher', function (Group $pools) {
|
|||
return new BrokerPool(publisher: $pools->get('publisher'));
|
||||
}, ['pools']);
|
||||
|
||||
Server::setResource('publisherRedis', function () {
|
||||
// Stub
|
||||
});
|
||||
Server::setResource('publisherDatabases', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherMigrations', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherStatsUsage', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('consumer', function (Group $pools) {
|
||||
return new BrokerPool(consumer: $pools->get('consumer'));
|
||||
}, ['pools']);
|
||||
|
||||
Server::setResource('consumerRedis', function () {
|
||||
// Stub
|
||||
});
|
||||
Server::setResource('consumerDatabases', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
Server::setResource('consumerMigrations', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
Server::setResource('consumerStatsUsage', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
Server::setResource('queueForStatsUsage', function (Publisher $publisher) {
|
||||
return new StatsUsage($publisher);
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@
|
|||
"utopia-php/swoole": "0.8.*",
|
||||
"utopia-php/system": "0.9.*",
|
||||
"utopia-php/telemetry": "0.1.*",
|
||||
"utopia-php/vcs": "0.10.*",
|
||||
"utopia-php/vcs": "0.11.*",
|
||||
"utopia-php/websocket": "0.3.*",
|
||||
"matomo/device-detector": "6.1.*",
|
||||
"dragonmantank/cron-expression": "3.3.*",
|
||||
|
|
|
|||
14
composer.lock
generated
14
composer.lock
generated
|
|
@ -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": "f53e1ccd394581428d9efcb53b46d479",
|
||||
"content-hash": "edbe5912c45e1f467f398541a75a77de",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
|
|
@ -4587,16 +4587,16 @@
|
|||
},
|
||||
{
|
||||
"name": "utopia-php/vcs",
|
||||
"version": "0.10.5",
|
||||
"version": "0.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/vcs.git",
|
||||
"reference": "b358439dc387f6097019eb83ebb9fc258fe9da05"
|
||||
"reference": "0e665eaa7d906168525bf6aac50b6bcc3e4fe528"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/b358439dc387f6097019eb83ebb9fc258fe9da05",
|
||||
"reference": "b358439dc387f6097019eb83ebb9fc258fe9da05",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/0e665eaa7d906168525bf6aac50b6bcc3e4fe528",
|
||||
"reference": "0e665eaa7d906168525bf6aac50b6bcc3e4fe528",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -4630,9 +4630,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/vcs/issues",
|
||||
"source": "https://github.com/utopia-php/vcs/tree/0.10.5"
|
||||
"source": "https://github.com/utopia-php/vcs/tree/0.11.0"
|
||||
},
|
||||
"time": "2025-06-10T15:01:16+00:00"
|
||||
"time": "2025-07-23T13:54:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/websocket",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class Mail extends Event
|
|||
protected string $name = '';
|
||||
protected string $subject = '';
|
||||
protected string $body = '';
|
||||
protected string $preview = '';
|
||||
protected array $smtp = [];
|
||||
protected array $variables = [];
|
||||
protected string $bodyTemplate = '';
|
||||
|
|
@ -93,6 +94,28 @@ class Mail extends Event
|
|||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets preview for the mail event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function setPreview(string $preview): self
|
||||
{
|
||||
$this->preview = $preview;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns preview for the mail event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPreview(string $preview): string
|
||||
{
|
||||
return $this->preview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets name for the mail event.
|
||||
*
|
||||
|
|
@ -122,7 +145,7 @@ class Mail extends Event
|
|||
* @param string $bodyTemplate
|
||||
* @return self
|
||||
*/
|
||||
public function setbodyTemplate(string $bodyTemplate): self
|
||||
public function setBodyTemplate(string $bodyTemplate): self
|
||||
{
|
||||
$this->bodyTemplate = $bodyTemplate;
|
||||
|
||||
|
|
@ -134,7 +157,7 @@ class Mail extends Event
|
|||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getbodyTemplate(): string
|
||||
public function getBodyTemplate(): string
|
||||
{
|
||||
return $this->bodyTemplate;
|
||||
}
|
||||
|
|
@ -409,6 +432,7 @@ class Mail extends Event
|
|||
'subject' => $this->subject,
|
||||
'bodyTemplate' => $this->bodyTemplate,
|
||||
'body' => $this->body,
|
||||
'preview' => $this->preview,
|
||||
'smtp' => $this->smtp,
|
||||
'variables' => $this->variables,
|
||||
'attachment' => $this->attachment,
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class Create extends Base
|
|||
->param('path', '/', new Text(2048), 'HTTP path of execution. Path can include query params. Default value is /', true)
|
||||
->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], true), 'HTTP method of execution. Default value is GET.', true)
|
||||
->param('headers', [], new AnyOf([new Assoc(), new Text(65535)], AnyOf::TYPE_MIXED), 'HTTP headers of execution. Defaults to empty.', true)
|
||||
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_MINUTES, offset: 60), 'Scheduled execution time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.', true)
|
||||
->param('scheduledAt', null, new Text(100), 'Scheduled execution time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.', true)
|
||||
->inject('response')
|
||||
->inject('request')
|
||||
->inject('project')
|
||||
|
|
@ -123,6 +123,13 @@ class Create extends Base
|
|||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Scheduled executions must run asynchronously. Set scheduledAt to a future date, or set async to true.');
|
||||
}
|
||||
|
||||
if (!is_null($scheduledAt)) {
|
||||
$validator = new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_MINUTES, offset: 60);
|
||||
if (!$validator->isValid($scheduledAt)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Execution schedule must be a valid date, and at least 1 minute from now');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array<string, mixed> $headers
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -444,7 +444,7 @@ class Builds extends Action
|
|||
Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr);
|
||||
|
||||
// Commit and push
|
||||
$exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git checkout -b ' . \escapeshellarg($branchName) . ' && git add . && git commit -m "Create ' . \escapeshellarg($resource->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr);
|
||||
$exit = Console::execute('git config --global user.email '. \escapeshellarg(APP_VCS_GITHUB_EMAIL) .' && git config --global user.name '. \escapeshellarg(APP_VCS_GITHUB_USERNAME) .' && cd ' . \escapeshellarg($tmpDirectory) . ' && git checkout -b ' . \escapeshellarg($branchName) . ' && git add . && git commit -m "Create ' . \escapeshellarg($resource->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr);
|
||||
|
||||
if ($exit !== 0) {
|
||||
throw new \Exception('Unable to push code repository: ' . $stderr);
|
||||
|
|
@ -516,7 +516,7 @@ class Builds extends Action
|
|||
->setPayload($deployment->getArrayCopy())
|
||||
->trigger();
|
||||
|
||||
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform);
|
||||
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime);
|
||||
}
|
||||
|
||||
/** Request the executor to build the code... */
|
||||
|
|
@ -532,7 +532,7 @@ class Builds extends Action
|
|||
->trigger();
|
||||
|
||||
if ($isVcsEnabled) {
|
||||
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform);
|
||||
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime);
|
||||
}
|
||||
|
||||
$deploymentModel = new Deployment();
|
||||
|
|
@ -980,6 +980,8 @@ class Builds extends Action
|
|||
throw new \Exception($screenshotError);
|
||||
}
|
||||
|
||||
$mimeType = "image/png";
|
||||
|
||||
foreach ($screenshots as $data) {
|
||||
$key = $data['key'];
|
||||
$screenshot = $data['screenshot'];
|
||||
|
|
@ -988,7 +990,7 @@ class Builds extends Action
|
|||
$fileName = $fileId . '.png';
|
||||
$path = $deviceForFiles->getPath($fileName);
|
||||
$path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root
|
||||
$success = $deviceForFiles->write($path, $screenshot, "image/png");
|
||||
$success = $deviceForFiles->write($path, $screenshot, $mimeType);
|
||||
|
||||
if (!$success) {
|
||||
throw new \Exception("Screenshot failed to save");
|
||||
|
|
@ -1005,10 +1007,10 @@ class Builds extends Action
|
|||
'name' => $fileName,
|
||||
'path' => $path,
|
||||
'signature' => $deviceForFiles->getFileHash($path),
|
||||
'mimeType' => $deviceForFiles->getFileMimeType($path),
|
||||
'mimeType' => $mimeType,
|
||||
'sizeOriginal' => \strlen($screenshot),
|
||||
'sizeActual' => $deviceForFiles->getFileSize($path),
|
||||
'algorithm' => Compression::GZIP,
|
||||
'algorithm' => Compression::NONE,
|
||||
'comment' => '',
|
||||
'chunksTotal' => 1,
|
||||
'chunksUploaded' => 1,
|
||||
|
|
@ -1017,7 +1019,7 @@ class Builds extends Action
|
|||
'openSSLTag' => null,
|
||||
'openSSLIV' => null,
|
||||
'search' => implode(' ', [$fileId, $fileName]),
|
||||
'metadata' => ['content_type' => $deviceForFiles->getFileMimeType($path)],
|
||||
'metadata' => ['content_type' => $mimeType],
|
||||
]);
|
||||
|
||||
Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getSequence(), $file));
|
||||
|
|
@ -1067,7 +1069,7 @@ class Builds extends Action
|
|||
->trigger();
|
||||
|
||||
if ($isVcsEnabled) {
|
||||
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform);
|
||||
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime);
|
||||
}
|
||||
|
||||
Console::success("Build id: $deploymentId created");
|
||||
|
|
@ -1285,7 +1287,7 @@ class Builds extends Action
|
|||
->trigger();
|
||||
|
||||
if ($isVcsEnabled) {
|
||||
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform);
|
||||
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime);
|
||||
}
|
||||
} finally {
|
||||
$queueForRealtime
|
||||
|
|
@ -1439,98 +1441,116 @@ class Builds extends Action
|
|||
Document $resource,
|
||||
string $deploymentId,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform
|
||||
Database $dbForPlatform,
|
||||
Realtime $queueForRealtime,
|
||||
): void {
|
||||
if ($resource->getAttribute('providerSilentMode', false) === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
$commentId = $deployment->getAttribute('providerCommentId', '');
|
||||
|
||||
if (!empty($providerCommitHash)) {
|
||||
$message = match ($status) {
|
||||
'ready' => 'Build succeeded.',
|
||||
'failed' => 'Build failed.',
|
||||
'processing' => 'Building...',
|
||||
default => $status
|
||||
};
|
||||
|
||||
$state = match ($status) {
|
||||
'ready' => 'success',
|
||||
'failed' => 'failure',
|
||||
'processing' => 'pending',
|
||||
default => $status
|
||||
};
|
||||
|
||||
$resourceName = $resource->getAttribute('name');
|
||||
$projectName = $project->getAttribute('name');
|
||||
|
||||
$name = "{$resourceName} ({$projectName})";
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
|
||||
|
||||
$projectId = $project->getId();
|
||||
$region = $project->getAttribute('region', 'default');
|
||||
$resourceId = $resource->getId();
|
||||
$providerTargetUrl = match ($resource->getCollection()) {
|
||||
'functions' => "{$protocol}://{$hostname}/console/project-{$region}-{$projectId}/functions/function-{$resourceId}",
|
||||
'sites' => "{$protocol}://{$hostname}/console/project-{$region}-{$projectId}/sites/site-{$resourceId}",
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
|
||||
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, $state, $message, $providerTargetUrl, $name);
|
||||
}
|
||||
|
||||
if (!empty($commentId)) {
|
||||
$retries = 0;
|
||||
|
||||
while (true) {
|
||||
$retries++;
|
||||
|
||||
try {
|
||||
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
|
||||
'$id' => $commentId
|
||||
]));
|
||||
break;
|
||||
} catch (\Throwable $err) {
|
||||
if ($retries >= 9) {
|
||||
throw $err;
|
||||
}
|
||||
|
||||
\sleep(1);
|
||||
}
|
||||
try {
|
||||
if ($resource->getAttribute('providerSilentMode', false) === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wrap in try/finally to ensure lock file gets deleted
|
||||
try {
|
||||
$resourceType = match($resource->getCollection()) {
|
||||
'functions' => 'function',
|
||||
'sites' => 'site',
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
$commentId = $deployment->getAttribute('providerCommentId', '');
|
||||
|
||||
if (!empty($providerCommitHash)) {
|
||||
$message = match ($status) {
|
||||
'ready' => 'Build succeeded.',
|
||||
'failed' => 'Build failed.',
|
||||
'processing' => 'Building...',
|
||||
default => $status
|
||||
};
|
||||
|
||||
$rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [
|
||||
Query::equal("projectInternalId", [$project->getSequence()]),
|
||||
Query::equal("type", ["deployment"]),
|
||||
Query::equal("deploymentInternalId", [$deployment->getSequence()]),
|
||||
]));
|
||||
$state = match ($status) {
|
||||
'ready' => 'success',
|
||||
'failed' => 'failure',
|
||||
'processing' => 'pending',
|
||||
default => $status
|
||||
};
|
||||
|
||||
$resourceName = $resource->getAttribute('name');
|
||||
$projectName = $project->getAttribute('name');
|
||||
|
||||
$name = "{$resourceName} ({$projectName})";
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$previewUrl = match($resource->getCollection()) {
|
||||
'functions' => '',
|
||||
'sites' => !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
|
||||
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
|
||||
|
||||
$projectId = $project->getId();
|
||||
$region = $project->getAttribute('region', 'default');
|
||||
$resourceId = $resource->getId();
|
||||
$providerTargetUrl = match ($resource->getCollection()) {
|
||||
'functions' => "{$protocol}://{$hostname}/console/project-{$region}-{$projectId}/functions/function-{$resourceId}",
|
||||
'sites' => "{$protocol}://{$hostname}/console/project-{$region}-{$projectId}/sites/site-{$resourceId}",
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
|
||||
$comment = new Comment();
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $commentId));
|
||||
$comment->addBuild($project, $resource, $resourceType, $status, $deployment->getId(), ['type' => 'logs'], $previewUrl);
|
||||
$github->updateComment($owner, $repositoryName, $commentId, $comment->generateComment());
|
||||
} finally {
|
||||
$dbForPlatform->deleteDocument('vcsCommentLocks', $commentId);
|
||||
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, $state, $message, $providerTargetUrl, $name);
|
||||
}
|
||||
|
||||
if (!empty($commentId)) {
|
||||
$retries = 0;
|
||||
|
||||
while (true) {
|
||||
$retries++;
|
||||
|
||||
try {
|
||||
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
|
||||
'$id' => $commentId
|
||||
]));
|
||||
break;
|
||||
} catch (\Throwable $err) {
|
||||
if ($retries >= 9) {
|
||||
throw $err;
|
||||
}
|
||||
|
||||
\sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap in try/finally to ensure lock file gets deleted
|
||||
try {
|
||||
$resourceType = match($resource->getCollection()) {
|
||||
'functions' => 'function',
|
||||
'sites' => 'site',
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
|
||||
$rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [
|
||||
Query::equal("projectInternalId", [$project->getSequence()]),
|
||||
Query::equal("type", ["deployment"]),
|
||||
Query::equal("deploymentInternalId", [$deployment->getSequence()]),
|
||||
]));
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$previewUrl = match($resource->getCollection()) {
|
||||
'functions' => '',
|
||||
'sites' => !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
|
||||
$comment = new Comment();
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $commentId));
|
||||
$comment->addBuild($project, $resource, $resourceType, $status, $deployment->getId(), ['type' => 'logs'], $previewUrl);
|
||||
$github->updateComment($owner, $repositoryName, $commentId, $comment->generateComment());
|
||||
} finally {
|
||||
$dbForPlatform->deleteDocument('vcsCommentLocks', $commentId);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("Git action failed:");
|
||||
Console::warning($th->getMessage());
|
||||
Console::warning($th->getTraceAsString());
|
||||
|
||||
$logs = $deployment->getAttribute('buildLogs', '');
|
||||
$date = \date('H:i:s');
|
||||
$logs .= "[90m[$date] [90m[[0mappwrite[90m][33m Git action failed. Deployment will continue. [0m\n";
|
||||
|
||||
$deployment->setAttribute('buildLogs', $logs);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
|
||||
$queueForRealtime
|
||||
->setPayload($deployment->getArrayCopy())
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ use Utopia\Database\Exception;
|
|||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Broker\Pool as BrokerPool;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
|
|
@ -26,7 +25,7 @@ abstract class ScheduleBase extends Action
|
|||
protected array $schedules = [];
|
||||
|
||||
protected BrokerPool $publisher;
|
||||
protected ?BrokerPool $publisherRedis = null;
|
||||
protected BrokerPool $publisherMigrations;
|
||||
|
||||
private ?Histogram $collectSchedulesTelemetryDuration = null;
|
||||
private ?Gauge $collectSchedulesTelemetryCount = null;
|
||||
|
|
@ -36,7 +35,7 @@ abstract class ScheduleBase extends Action
|
|||
abstract public static function getName(): string;
|
||||
abstract public static function getSupportedResource(): string;
|
||||
abstract public static function getCollectionId(): string;
|
||||
abstract protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void;
|
||||
abstract protected function enqueueResources(Database $dbForPlatform, callable $getProjectDB): void;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
|
@ -44,7 +43,8 @@ abstract class ScheduleBase extends Action
|
|||
|
||||
$this
|
||||
->desc("Execute {$type}s scheduled in Appwrite")
|
||||
->inject('pools')
|
||||
->inject('publisher')
|
||||
->inject('publisherMigrations')
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('telemetry')
|
||||
|
|
@ -67,18 +67,13 @@ abstract class ScheduleBase extends Action
|
|||
* 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute
|
||||
* 3. Create timer that prepares coroutines for soon-to-execute schedules. When it's ready, coroutine sleeps until exact time before sending request to worker.
|
||||
*/
|
||||
public function action(Group $pools, Database $dbForPlatform, callable $getProjectDB, Telemetry $telemetry): void
|
||||
public function action(BrokerPool $publisher, BrokerPool $publisherMigrations, Database $dbForPlatform, callable $getProjectDB, Telemetry $telemetry): void
|
||||
{
|
||||
Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1');
|
||||
Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started');
|
||||
|
||||
$this->publisher = new BrokerPool($pools->get('publisher'));
|
||||
|
||||
try {
|
||||
$this->publisherRedis = new BrokerPool($pools->get('publisherRedis'));
|
||||
} catch (\Throwable) {
|
||||
$this->publisherRedis = null;
|
||||
}
|
||||
$this->publisher = $publisher;
|
||||
$this->publisherMigrations = $publisherMigrations;
|
||||
|
||||
$this->scheduleTelemetryCount = $telemetry->createGauge('task.schedule.count');
|
||||
$this->collectSchedulesTelemetryDuration = $telemetry->createHistogram('task.schedule.collect_schedules.duration', 's');
|
||||
|
|
@ -101,7 +96,7 @@ abstract class ScheduleBase extends Action
|
|||
|
||||
while (true) {
|
||||
try {
|
||||
go(fn () => $this->enqueueResources($pools, $dbForPlatform, $getProjectDB));
|
||||
go(fn () => $this->enqueueResources($dbForPlatform, $getProjectDB));
|
||||
$this->scheduleTelemetryCount->record(count($this->schedules), ['resourceType' => static::getSupportedResource()]);
|
||||
sleep(static::ENQUEUE_TIMER);
|
||||
} catch (\Throwable $th) {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ namespace Appwrite\Platform\Tasks;
|
|||
use Appwrite\Event\Func;
|
||||
use Swoole\Coroutine as Co;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\System\System;
|
||||
|
||||
class ScheduleExecutions extends ScheduleBase
|
||||
{
|
||||
|
|
@ -28,17 +26,11 @@ class ScheduleExecutions extends ScheduleBase
|
|||
return 'executions';
|
||||
}
|
||||
|
||||
protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
|
||||
protected function enqueueResources(Database $dbForPlatform, callable $getProjectDB): void
|
||||
{
|
||||
$intervalEnd = (new \DateTime())->modify('+' . self::ENQUEUE_TIMER . ' seconds');
|
||||
|
||||
$isRedisFallback = \str_contains(System::getEnv('_APP_WORKER_REDIS_FALLBACK', ''), 'functions');
|
||||
|
||||
$queueForFunctions = new Func(
|
||||
$isRedisFallback
|
||||
? $this->publisherRedis
|
||||
: $this->publisher
|
||||
);
|
||||
$queueForFunctions = new Func($this->publisher);
|
||||
|
||||
foreach ($this->schedules as $schedule) {
|
||||
if (!$schedule['active']) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ use Utopia\CLI\Console;
|
|||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\System\System;
|
||||
|
||||
class ScheduleFunctions extends ScheduleBase
|
||||
{
|
||||
|
|
@ -32,7 +31,7 @@ class ScheduleFunctions extends ScheduleBase
|
|||
return 'functions';
|
||||
}
|
||||
|
||||
protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
|
||||
protected function enqueueResources(Database $dbForPlatform, callable $getProjectDB): void
|
||||
{
|
||||
$timerStart = \microtime(true);
|
||||
$time = DateTime::now();
|
||||
|
|
@ -91,13 +90,7 @@ class ScheduleFunctions extends ScheduleBase
|
|||
|
||||
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
|
||||
|
||||
$isRedisFallback = \str_contains(System::getEnv('_APP_WORKER_REDIS_FALLBACK', ''), 'functions');
|
||||
|
||||
$queueForFunctions = new Func(
|
||||
$isRedisFallback
|
||||
? $this->publisherRedis
|
||||
: $this->publisher
|
||||
);
|
||||
$queueForFunctions = new Func($this->publisher);
|
||||
|
||||
$queueForFunctions
|
||||
->setType('schedule')
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ namespace Appwrite\Platform\Tasks;
|
|||
|
||||
use Appwrite\Event\Messaging;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Pools\Group;
|
||||
|
||||
class ScheduleMessages extends ScheduleBase
|
||||
{
|
||||
|
|
@ -26,7 +25,7 @@ class ScheduleMessages extends ScheduleBase
|
|||
return 'messages';
|
||||
}
|
||||
|
||||
protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
|
||||
protected function enqueueResources(Database $dbForPlatform, callable $getProjectDB): void
|
||||
{
|
||||
foreach ($this->schedules as $schedule) {
|
||||
if (!$schedule['active']) {
|
||||
|
|
|
|||
|
|
@ -382,12 +382,14 @@ class Certificates extends Action
|
|||
];
|
||||
|
||||
$subject = \sprintf($locale->getText("emails.certificate.subject"), $domain);
|
||||
$preview = \sprintf($locale->getText("emails.certificate.preview"), $domain);
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setName('Appwrite Administrator')
|
||||
->setbodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl')
|
||||
->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl')
|
||||
->setVariables($emailVariables)
|
||||
->setRecipient(System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')))
|
||||
->trigger();
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ use Utopia\System\System;
|
|||
|
||||
class Mails extends Action
|
||||
{
|
||||
protected int $previewMaxLen = 150;
|
||||
|
||||
protected string $whitespaceCodes = ' ‌​‍‎‏';
|
||||
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'mails';
|
||||
|
|
@ -74,6 +79,7 @@ class Mails extends Action
|
|||
$variables['host'] = $protocol . '://' . $hostname;
|
||||
$name = $payload['name'];
|
||||
$body = $payload['body'];
|
||||
$preview = $payload['preview'] ?? '';
|
||||
|
||||
$variables['subject'] = $subject;
|
||||
$variables['year'] = date("Y");
|
||||
|
|
@ -92,6 +98,27 @@ class Mails extends Action
|
|||
foreach ($this->richTextParams as $key => $value) {
|
||||
$bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: false);
|
||||
}
|
||||
|
||||
$previewWhitespace = '';
|
||||
|
||||
if (!empty($preview)) {
|
||||
$previewTemplate = Template::fromString($preview);
|
||||
foreach ($variables as $key => $value) {
|
||||
$previewTemplate->setParam('{{' . $key . '}}', $value);
|
||||
}
|
||||
// render() will return the subject in <p> tags, so use strip_tags() to remove them
|
||||
$preview = \strip_tags($previewTemplate->render());
|
||||
|
||||
$previewLen = strlen($preview);
|
||||
if ($previewLen < $this->previewMaxLen) {
|
||||
$previewWhitespace = str_repeat($this->whitespaceCodes, $this->previewMaxLen - $previewLen);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$bodyTemplate->setParam('{{preview}}', $preview);
|
||||
$bodyTemplate->setParam('{{previewWhitespace}}', $previewWhitespace, false);
|
||||
|
||||
$body = $bodyTemplate->render();
|
||||
|
||||
$subjectTemplate = Template::fromString($subject);
|
||||
|
|
|
|||
|
|
@ -241,6 +241,7 @@ class Webhooks extends Action
|
|||
|
||||
// TODO: Use setbodyTemplate once #7307 is merged
|
||||
$subject = 'Webhook deliveries have been paused';
|
||||
$preview = 'Webhook deliveries to your endpoint have been paused.';
|
||||
$body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl');
|
||||
|
||||
$body
|
||||
|
|
@ -250,6 +251,7 @@ class Webhooks extends Action
|
|||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body->render());
|
||||
|
||||
foreach ($users as $user) {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class Template extends View
|
|||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function render($minify = true): string
|
||||
public function render($minify = true, $useContent = false): string
|
||||
{
|
||||
if ($this->rendered) { // Don't render any template
|
||||
return '';
|
||||
|
|
@ -72,7 +72,7 @@ class Template extends View
|
|||
if (\is_readable($this->path)) {
|
||||
$template = \file_get_contents($this->path); // Include template file
|
||||
} elseif (!empty($this->content)) {
|
||||
$template = $this->print($this->content, self::FILTER_NL2P);
|
||||
$template = !$useContent ? $this->print($this->content, self::FILTER_NL2P) : $this->content;
|
||||
} else {
|
||||
throw new Exception('"' . $this->path . '" template is not readable or not found');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ class ProviderRepository extends Model
|
|||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('defaultBranch', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => "VCS (Version Control System) repository's default branch name.",
|
||||
'default' => '',
|
||||
'example' => 'main',
|
||||
])
|
||||
->addRule('pushedAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Last commit date in ISO 8601 format.',
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ trait AccountBase
|
|||
$userId = $response['body']['userId'];
|
||||
|
||||
$lastEmail = $this->getLastEmail();
|
||||
|
||||
$this->assertEquals('otpuser@appwrite.io', $lastEmail['to'][0]['address']);
|
||||
$this->assertEquals('OTP for ' . $this->getProject()['name'] . ' Login', $lastEmail['subject']);
|
||||
|
||||
|
|
@ -178,6 +179,7 @@ trait AccountBase
|
|||
$code = ($matches[0] ?? [])[0] ?? '';
|
||||
|
||||
$this->assertNotEmpty($code);
|
||||
$this->assertStringContainsStringIgnoringCase('Use OTP ' . $code . ' to sign in to '. $this->getProject()['name'] . '. Expires in 15 minutes.', $lastEmail['text']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/token', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
|
|
|
|||
|
|
@ -779,6 +779,7 @@ class AccountCustomClientTest extends Scope
|
|||
$this->assertEquals($email, $lastEmail['to'][0]['address']);
|
||||
$this->assertEquals($name, $lastEmail['to'][0]['name']);
|
||||
$this->assertEquals('Account Verification', $lastEmail['subject']);
|
||||
$this->assertStringContainsStringIgnoringCase('Verify your email to activate your ' . $this->getProject()['name'] . ' account.', $lastEmail['text']);
|
||||
|
||||
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
|
||||
$verification = $tokens['secret'];
|
||||
|
|
@ -1082,6 +1083,8 @@ class AccountCustomClientTest extends Scope
|
|||
$this->assertEquals($email, $lastEmail['to'][0]['address']);
|
||||
$this->assertEquals($name, $lastEmail['to'][0]['name']);
|
||||
$this->assertEquals('Password Reset', $lastEmail['subject']);
|
||||
$this->assertStringContainsStringIgnoringCase('Reset your ' . $this->getProject()['name'] . ' password using the link.', $lastEmail['text']);
|
||||
|
||||
|
||||
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
|
||||
|
||||
|
|
@ -1286,6 +1289,7 @@ class AccountCustomClientTest extends Scope
|
|||
$this->assertNotEmpty($response['body']['expire']);
|
||||
$this->assertEmpty($response['body']['secret']);
|
||||
$this->assertEmpty($response['body']['phrase']);
|
||||
$this->assertStringContainsStringIgnoringCase('New login detected on '. $this->getProject()['name'], $lastEmail['text']);
|
||||
|
||||
$userId = $response['body']['userId'];
|
||||
|
||||
|
|
@ -2545,6 +2549,7 @@ class AccountCustomClientTest extends Scope
|
|||
$lastEmail = $this->getLastEmail();
|
||||
$this->assertEquals($email, $lastEmail['to'][0]['address']);
|
||||
$this->assertEquals($this->getProject()['name'] . ' Login', $lastEmail['subject']);
|
||||
$this->assertStringContainsStringIgnoringCase('Sign in to '. $this->getProject()['name'] . ' with your secure link. Expires in 1 hour.', $lastEmail['text']);
|
||||
$this->assertStringNotContainsStringIgnoringCase('security phrase', $lastEmail['text']);
|
||||
|
||||
$token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 64);
|
||||
|
|
|
|||
|
|
@ -92,12 +92,51 @@ class SitesConsoleClientTest extends Scope
|
|||
|
||||
$this->assertNotEquals($screenshotDarkHash, $screenshotHash);
|
||||
|
||||
$screenshotId = $deployment['body']['screenshotLight'];
|
||||
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console");
|
||||
$this->assertEquals(404, $file['headers']['status-code']);
|
||||
|
||||
$screenshotId = $deployment['body']['screenshotDark'];
|
||||
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console");
|
||||
$this->assertEquals(404, $file['headers']['status-code']);
|
||||
|
||||
// Verify previews
|
||||
$screenshotId = $deployment['body']['screenshotLight'];
|
||||
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/preview?project=console", array_merge($this->getHeaders(), [
|
||||
'x-appwrite-mode' => 'default' // NOT ADMIN!
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $file['headers']['status-code']);
|
||||
$this->assertNotEmpty(200, $file['body']);
|
||||
$this->assertGreaterThan(1, $file['headers']['content-length']);
|
||||
$this->assertEquals('image/png', $file['headers']['content-type']);
|
||||
|
||||
$screenshotHash = \md5($file['body']);
|
||||
$this->assertNotEmpty($screenshotHash);
|
||||
|
||||
$screenshotId = $deployment['body']['screenshotDark'];
|
||||
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/preview?project=console", array_merge($this->getHeaders(), [
|
||||
'x-appwrite-mode' => 'default' // NOT ADMIN!
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $file['headers']['status-code']);
|
||||
$this->assertNotEmpty(200, $file['body']);
|
||||
$this->assertGreaterThan(1, $file['headers']['content-length']);
|
||||
$this->assertEquals('image/png', $file['headers']['content-type']);
|
||||
|
||||
$screenshotDarkHash = \md5($file['body']);
|
||||
$this->assertNotEmpty($screenshotDarkHash);
|
||||
|
||||
$this->assertNotEquals($screenshotDarkHash, $screenshotHash);
|
||||
|
||||
$screenshotId = $deployment['body']['screenshotLight'];
|
||||
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/preview?project=console");
|
||||
$this->assertEquals(404, $file['headers']['status-code']);
|
||||
|
||||
$screenshotId = $deployment['body']['screenshotDark'];
|
||||
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/preview?project=console");
|
||||
$this->assertEquals(404, $file['headers']['status-code']);
|
||||
|
||||
$this->cleanupSite($siteId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue