Merge branch '1.4.x' of github.com:appwrite/appwrite into disallow-personal-data

This commit is contained in:
Christy Jacob
2023-07-20 17:19:18 +04:00
38 changed files with 1939 additions and 441 deletions
+42 -2
View File
@@ -698,6 +698,28 @@ $collections = [
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('smtp'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => [],
'array' => false,
'filters' => ['json', 'encrypt'],
],
[
'$id' => ID::custom('templates'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 1000000, // TODO make sure size fits
'signed' => true,
'required' => false,
'default' => [],
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('auths'),
'type' => Database::VAR_STRING,
@@ -1441,7 +1463,18 @@ $collections = [
'default' => null,
'array' => false,
'filters' => ['userSearch'],
]
],
[
'$id' => ID::custom('accessedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[
@@ -1506,7 +1539,14 @@ $collections = [
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
]
],
[
'$id' => '_key_accessedAt',
'type' => Database::INDEX_KEY,
'attributes' => ['accessedAt'],
'lengths' => [],
'orders' => [],
],
],
],
+10
View File
@@ -559,6 +559,16 @@ return [
'description' => 'The project key has expired. Please generate a new key using the Appwrite console.',
'code' => 401,
],
Exception::PROJECT_SMTP_CONFIG_INVALID => [
'name' => Exception::PROJECT_SMTP_CONFIG_INVALID,
'description' => 'Provided SMTP config is invalid.',
'code' => 400,
],
Exception::PROJECT_TEMPLATE_DEFAULT_DELETION => [
'name' => Exception::PROJECT_TEMPLATE_DEFAULT_DELETION,
'description' => 'The default template for the project cannot be deleted.',
'code' => 401,
],
Exception::WEBHOOK_NOT_FOUND => [
'name' => Exception::WEBHOOK_NOT_FOUND,
'description' => 'Webhook with the requested ID could not be found.',
+537 -72
View File
@@ -1,76 +1,541 @@
<?php
/**
* ISO 639-1 standard language codes
* https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
*
* Source:
* https://www.andiamo.co.uk/resources/iso-language-codes/
*
*/
return [
'af', // Afrikaans
'ar', // Arabic
'as', // Assamese
'az', // Azerbaijani
'be', // Belarusian
'bg', // Bulgarian
'bh', // Bihari
'bn', // Bengali
'bs', // Bosnian
'ca', // Catalan
'cs', // Czech
'da', // Danish
'de', // German
'en', // English
'eo', // Esperanto
'es', // Spanish
'fa', // Farsi/Persian
'fi', // Finnish
'fo', // Faroese
'fr', // French
'el', // Greek
'ga', // Irish
'gu', // Gujrati
'he', // Hebrew
'hi', // Hindi,
'hr', // Croatian
'hu', // Hungarian
'hy', // Armenian
'id', // Indonesian
'is', // Icelandic
'it', // Italian
'ja', // Japanese
'jv', // Javanese
'kn', // Kannada
'km', // Khmer
'ko', // Korean
'la', // Latin
'lb', // Luxembourgish
'lt', // Lithuanian
'lv', // Latvian
'ml', // Malayalam
'mr', // Marathi
'ms', // Malay
'nb', // Norwegian bokmål
'nl', // Dutch
'nn', // Norwegian nynorsk
'ne', // Nepali
'or', // Oriya
'tl', // Filipino
'pl', // Polish
'pt-br', // Portuguese - Brazil
'pt-pt', // Portuguese - Portugal
'pa', // Punjabi
'ro', // Romanian
'ru', // Russian
'sa', //Sanskrit
'sd', // Sindhi
'si', // Sinhala
'sk', // Slovakia
'sl', // Slovenian
'sn', // Shona
'sq', // Albanian
'sv', // Swedish
'ta', // Tamil
'te', // Telugu
'th', // Thai
'tr', // Turkish
'uk', // Ukrainian
'ur', // Urdu
'vi', // Vietnamese
'zh-cn', // Chinese - China
'zh-tw', // Chinese - Taiwan
[
"code" => "af",
"name" => "Afrikaans",
],
[
"code" => "ar-ae",
"name" => "Arabic (U.A.E.)",
],
[
"code" => "ar-bh",
"name" => "Arabic (Bahrain)",
],
[
"code" => "ar-dz",
"name" => "Arabic (Algeria)",
],
[
"code" => "ar-eg",
"name" => "Arabic (Egypt)",
],
[
"code" => "ar-iq",
"name" => "Arabic (Iraq)",
],
[
"code" => "ar-jo",
"name" => "Arabic (Jordan)",
],
[
"code" => "ar-kw",
"name" => "Arabic (Kuwait)",
],
[
"code" => "ar-lb",
"name" => "Arabic (Lebanon)",
],
[
"code" => "ar-ly",
"name" => "Arabic (Libya)",
],
[
"code" => "ar-ma",
"name" => "Arabic (Morocco)",
],
[
"code" => "ar-om",
"name" => "Arabic (Oman)",
],
[
"code" => "ar-qa",
"name" => "Arabic (Qatar)",
],
[
"code" => "ar-sa",
"name" => "Arabic (Saudi Arabia)",
],
[
"code" => "ar-sy",
"name" => "Arabic (Syria)",
],
[
"code" => "ar-tn",
"name" => "Arabic (Tunisia)",
],
[
"code" => "ar-ye",
"name" => "Arabic (Yemen)",
],
[
"code" => "as",
"name" => "Assamese",
],
[
"code" => "az",
"name" => "Azerbaijani",
],
[
"code" => "be",
"name" => "Belarusian",
],
[
"code" => "bg",
"name" => "Bulgarian",
],
[
"code" => "bh",
"name" => "Bihari",
],
[
"code" => "bn",
"name" => "Bengali",
],
[
"code" => "bs",
"name" => "Bosnian",
],
[
"code" => "ca",
"name" => "Catalan",
],
[
"code" => "cs",
"name" => "Czech",
],
[
"code" => "cy",
"name" => "Welsh",
],
[
"code" => "da",
"name" => "Danish",
],
[
"code" => "de",
"name" => "German (Standard)",
],
[
"code" => "de-at",
"name" => "German (Austria)",
],
[
"code" => "de-ch",
"name" => "German (Switzerland)",
],
[
"code" => "de-li",
"name" => "German (Liechtenstein)",
],
[
"code" => "de-lu",
"name" => "German (Luxembourg)",
],
[
"code" => "el",
"name" => "Greek",
],
[
"code" => "en",
"name" => "English",
],
[
"code" => "en-au",
"name" => "English (Australia)",
],
[
"code" => "en-bz",
"name" => "English (Belize)",
],
[
"code" => "en-ca",
"name" => "English (Canada)",
],
[
"code" => "en-gb",
"name" => "English (United Kingdom)",
],
[
"code" => "en-ie",
"name" => "English (Ireland)",
],
[
"code" => "en-jm",
"name" => "English (Jamaica)",
],
[
"code" => "en-nz",
"name" => "English (New Zealand)",
],
[
"code" => "en-tt",
"name" => "English (Trinidad)",
],
[
"code" => "en-us",
"name" => "English (United States)",
],
[
"code" => "en-za",
"name" => "English (South Africa)",
],
[
"code" => "eo",
"name" => "Esperanto",
],
[
"code" => "es",
"name" => "Spanish (Spain)",
],
[
"code" => "es-ar",
"name" => "Spanish (Argentina)",
],
[
"code" => "es-bo",
"name" => "Spanish (Bolivia)",
],
[
"code" => "es-cl",
"name" => "Spanish (Chile)",
],
[
"code" => "es-co",
"name" => "Spanish (Colombia)",
],
[
"code" => "es-cr",
"name" => "Spanish (Costa Rica)",
],
[
"code" => "es-do",
"name" => "Spanish (Dominican Republic)",
],
[
"code" => "es-ec",
"name" => "Spanish (Ecuador)",
],
[
"code" => "es-gt",
"name" => "Spanish (Guatemala)",
],
[
"code" => "es-hn",
"name" => "Spanish (Honduras)",
],
[
"code" => "es-mx",
"name" => "Spanish (Mexico)",
],
[
"code" => "es-ni",
"name" => "Spanish (Nicaragua)",
],
[
"code" => "es-pa",
"name" => "Spanish (Panama)",
],
[
"code" => "es-pe",
"name" => "Spanish (Peru)",
],
[
"code" => "es-pr",
"name" => "Spanish (Puerto Rico)",
],
[
"code" => "es-py",
"name" => "Spanish (Paraguay)",
],
[
"code" => "es-sv",
"name" => "Spanish (El Salvador)",
],
[
"code" => "es-uy",
"name" => "Spanish (Uruguay)",
],
[
"code" => "es-ve",
"name" => "Spanish (Venezuela)",
],
[
"code" => "et",
"name" => "Estonian",
],
[
"code" => "eu",
"name" => "Basque",
],
[
"code" => "fa",
"name" => "Farsi",
],
[
"code" => "fi",
"name" => "Finnish",
],
[
"code" => "fo",
"name" => "Faeroese",
],
[
"code" => "fr",
"name" => "French (Standard)",
],
[
"code" => "fr-be",
"name" => "French (Belgium)",
],
[
"code" => "fr-ca",
"name" => "French (Canada)",
],
[
"code" => "fr-ch",
"name" => "French (Switzerland)",
],
[
"code" => "fr-lu",
"name" => "French (Luxembourg)",
],
[
"code" => "ga",
"name" => "Irish",
],
[
"code" => "gd",
"name" => "Gaelic (Scotland)",
],
[
"code" => "he",
"name" => "Hebrew",
],
[
"code" => "hi",
"name" => "Hindi",
],
[
"code" => "hr",
"name" => "Croatian",
],
[
"code" => "hu",
"name" => "Hungarian",
],
[
"code" => "id",
"name" => "Indonesian",
],
[
"code" => "is",
"name" => "Icelandic",
],
[
"code" => "it",
"name" => "Italian (Standard)",
],
[
"code" => "it-ch",
"name" => "Italian (Switzerland)",
],
[
"code" => "ja",
"name" => "Japanese",
],
[
"code" => "ji",
"name" => "Yiddish",
],
[
"code" => "ko",
"name" => "Korean",
],
[
"code" => "ko",
"name" => "Korean (Johab)",
],
[
"code" => "ku",
"name" => "Kurdish",
],
[
"code" => "lt",
"name" => "Lithuanian",
],
[
"code" => "lv",
"name" => "Latvian",
],
[
"code" => "mk",
"name" => "Macedonian (FYROM)",
],
[
"code" => "ml",
"name" => "Malayalam",
],
[
"code" => "ms",
"name" => "Malaysian",
],
[
"code" => "mt",
"name" => "Maltese",
],
[
"code" => "nb",
"name" => "Norwegian (Bokmål)",
],
[
"code" => "ne",
"name" => "Nepali",
],
[
"code" => "nl",
"name" => "Dutch (Standard)",
],
[
"code" => "nl-be",
"name" => "Dutch (Belgium)",
],
[
"code" => "nn",
"name" => "Norwegian (Nynorsk)",
],
[
"code" => "no",
"name" => "Norwegian",
],
[
"code" => "pa",
"name" => "Punjabi",
],
[
"code" => "pl",
"name" => "Polish",
],
[
"code" => "pt",
"name" => "Portuguese (Portugal)",
],
[
"code" => "pt-br",
"name" => "Portuguese (Brazil)",
],
[
"code" => "rm",
"name" => "Rhaeto-Romanic",
],
[
"code" => "ro",
"name" => "Romanian",
],
[
"code" => "ro-md",
"name" => "Romanian (Republic of Moldova)",
],
[
"code" => "ru",
"name" => "Russian",
],
[
"code" => "ru-md",
"name" => "Russian (Republic of Moldova)",
],
[
"code" => "sb",
"name" => "Sorbian",
],
[
"code" => "sk",
"name" => "Slovak",
],
[
"code" => "sl",
"name" => "Slovenian",
],
[
"code" => "sq",
"name" => "Albanian",
],
[
"code" => "sr",
"name" => "Serbian",
],
[
"code" => "sv",
"name" => "Swedish",
],
[
"code" => "sv-fi",
"name" => "Swedish (Finland)",
],
[
"code" => "th",
"name" => "Thai",
],
[
"code" => "tn",
"name" => "Tswana",
],
[
"code" => "tr",
"name" => "Turkish",
],
[
"code" => "ts",
"name" => "Tsonga",
],
[
"code" => "ua",
"name" => "Ukrainian",
],
[
"code" => "ur",
"name" => "Urdu",
],
[
"code" => "ve",
"name" => "Venda",
],
[
"code" => "vi",
"name" => "Vietnamese",
],
[
"code" => "xh",
"name" => "Xhosa",
],
[
"code" => "zh-cn",
"name" => "Chinese (PRC)",
],
[
"code" => "zh-hk",
"name" => "Chinese (Hong Kong)",
],
[
"code" => "zh-sg",
"name" => "Chinese (Singapore)",
],
[
"code" => "zh-tw",
"name" => "Chinese (Taiwan)",
],
[
"code" => "zu",
"name" => "Zulu",
],
];
+15
View File
@@ -0,0 +1,15 @@
<?php
return [
'email' => [
'verification',
'magicSession',
'recovery',
'invitation',
],
'sms' => [
'verification',
'login',
'invitation'
]
];
+69 -149
View File
@@ -2,164 +2,84 @@
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{{subject}}</title>
<style>
body {
background-color: {{bg-body}};
color: {{text-content}};
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Poppins:wght@500;600&display=swap"
rel="stylesheet">
<style>
a { color:currentColor; }
body {
padding: 32px;
color: #616B7C;
font-size: 15px;
font-family: 'Inter', sans-serif;
line-height: 15px;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
}
table {
width: 100%;
border-spacing: 0 !important;
}
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
table,
tr,
th,
td {
margin: 0;
padding: 0;
}
.body {
background-color: {{bg-body}};
width: 100%;
}
td {
vertical-align: top;
}
.container {
display: block;
margin: 0 auto !important;
max-width: 580px;
padding: 10px;
width: 580px;
}
h* {
font-family: 'Poppins', sans-serif;
}
.content {
box-sizing: border-box;
display: block;
margin: 0 auto;
max-width: 580px;
padding: 10px;
color: {{text-content}};
}
.main {
background: {{bg-content}};
border-radius: 10px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 30px 30px 15px 30px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
p {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
}
a {
word-break: break-all;
}
@media only screen and (max-width: 620px) {
.container {
padding: 0;
width: 100%;
}
}
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
}
</style>
hr {
border: none;
border-top: 1px solid #E8E9F0;
}
</style>
</head>
<body style="direction: {{direction}}">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<table role="presentation" class="main">
<tr>
<td class="wrapper">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>{{hello}}</p>
<p>{{body}}</p>
<a href="{{redirect}}" target="_blank">{{redirect}}</a>
<p></br>{{footer}}</p>
<p>{{thanks}}
</br>
{{signature}}
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!-- <div style="text-align: center; line-height: 25px; margin: 15px 0; font-size: 12px; color: #40404c;">
<a href="https://appwrite.io" style="text-decoration: none; color: #40404c;">Powered by <img src="https://appwrite.io/images/appwrite-footer-light.svg" height="15" style="margin: -3px 0" /></a>
</div> -->
</td>
<td>&nbsp;</td>
</tr>
</table>
<div style="max-width:650px; word-wrap: break-wrod; overflow-wrap: break-word;
word-break: break-all; margin:0 auto;">
<table style="margin-top: 32px">
<tr>
<td>
<h1>
{{subject}}
</h1>
</td>
</tr>
</table>
<table style="margin-top: 40px">
<tr>
<td>
<p>{{hello}}</p>
<p>{{body}}</p>
<a href="{{redirect}}" target="_blank">{{redirect}}</a>
<p><br />{{footer}}</p>
<br />
<p>{{thanks}}
<br />
{{signature}}
</p>
</td>
</tr>
</table>
</div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
{{token}}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+146 -60
View File
@@ -71,10 +71,11 @@ App::post('/v1/account')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('request')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $project, Database $dbForProject, Event $events) {
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $events) {
$email = \strtolower($email);
if ('console' === $project->getId()) {
$whitelistEmails = $project->getAttribute('authWhitelistEmails');
@@ -110,7 +111,7 @@ App::post('/v1/account')
$password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
try {
$userId = $userId == 'unique()' ? ID::unique() : $userId;
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
$user->setAttributes([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
@@ -132,8 +133,10 @@ App::post('/v1/account')
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name])
])));
'search' => implode(' ', [$userId, $email, $name]),
'accessedAt' => DateTime::now(), // Add this here to make sure it's returned in the response
]);
Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@@ -174,12 +177,13 @@ App::post('/v1/account/sessions/email')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('project')
->inject('locale')
->inject('geodb')
->inject('events')
->action(function (string $email, string $password, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $events) {
->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $events) {
$email = \strtolower($email);
$protocol = $request->getProtocol();
@@ -196,6 +200,8 @@ App::post('/v1/account/sessions/email')
throw new Exception(Exception::USER_BLOCKED); // User is in status blocked
}
$user->setAttributes($profile->getArrayCopy());
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$detector = new Detector($request->getUserAgent('UNKNOWN'));
@@ -205,8 +211,8 @@ App::post('/v1/account/sessions/email')
$session = new Document(array_merge(
[
'$id' => ID::unique(),
'userId' => $profile->getId(),
'userInternalId' => $profile->getInternalId(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $email,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
@@ -219,35 +225,35 @@ App::post('/v1/account/sessions/email')
$detector->getDevice()
));
Authorization::setRole(Role::user($profile->getId())->toString());
Authorization::setRole(Role::user($user->getId())->toString());
// Re-hash if not using recommended algo
if ($profile->getAttribute('hash') !== Auth::DEFAULT_ALGO) {
$profile
if ($user->getAttribute('hash') !== Auth::DEFAULT_ALGO) {
$user
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
$dbForProject->updateDocument('users', $profile->getId(), $profile);
$dbForProject->updateDocument('users', $user->getId(), $user);
}
$dbForProject->deleteCachedDocument('users', $profile->getId());
$dbForProject->deleteCachedDocument('users', $user->getId());
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
Permission::read(Role::user($profile->getId())),
Permission::update(Role::user($profile->getId())),
Permission::delete(Role::user($profile->getId())),
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($profile->getId(), $secret)]))
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]))
;
}
$response
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($profile->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($profile->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
@@ -260,7 +266,7 @@ App::post('/v1/account/sessions/email')
;
$events
->setParam('userId', $profile->getId())
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
;
@@ -484,10 +490,15 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
}
}
$user = ($user->isEmpty()) ? $dbForProject->findOne('sessions', [ // Get user by provider id
Query::equal('provider', [$provider]),
Query::equal('providerUid', [$oauth2ID]),
]) : $user;
if ($user->isEmpty()) {
$session = $dbForProject->findOne('sessions', [ // Get user by provider id
Query::equal('provider', [$provider]),
Query::equal('providerUid', [$oauth2ID]),
]);
if ($session !== false && !$session->isEmpty()) {
$user->setAttributes($dbForProject->getDocument('users', $session->getAttribute('userId'))->getArrayCopy());
}
}
if ($user === false || $user->isEmpty()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
$name = $oauth2->getUserName($accessToken);
@@ -498,9 +509,12 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
*/
$isVerified = $oauth2->isEmailVerified($accessToken);
$user = $dbForProject->findOne('users', [
$userWithEmail = $dbForProject->findOne('users', [
Query::equal('email', [$email]),
]);
if ($userWithEmail !== false && !$userWithEmail->isEmpty()) {
$user->setAttributes($userWithEmail->getArrayCopy());
}
if ($user === false || $user->isEmpty()) { // Last option -> create the user, generate random password
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
@@ -518,7 +532,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
try {
$userId = ID::unique();
$password = Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
$user->setAttributes([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
@@ -541,7 +555,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name])
])));
]);
Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@@ -653,12 +668,13 @@ App::post('/v1/account/sessions/magic-url')
->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
->inject('request')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('locale')
->inject('events')
->inject('mails')
->action(function (string $userId, string $email, string $url, Request $request, Response $response, Document $project, Database $dbForProject, Locale $locale, Event $events, Mail $mails) {
->action(function (string $userId, string $email, string $url, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $events, Mail $mails) {
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
@@ -668,9 +684,10 @@ App::post('/v1/account/sessions/magic-url')
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$user = $dbForProject->findOne('users', [Query::equal('email', [$email])]);
if (!$user) {
$result = $dbForProject->findOne('users', [Query::equal('email', [$email])]);
if ($result !== false && !$result->isEmpty()) {
$user->setAttributes($result->getArrayCopy());
} else {
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if ($limit !== 0) {
@@ -683,7 +700,7 @@ App::post('/v1/account/sessions/magic-url')
$userId = $userId === 'unique()' ? ID::unique() : $userId;
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
$user->setAttributes([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
@@ -704,7 +721,9 @@ App::post('/v1/account/sessions/magic-url')
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email])
])));
]);
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
}
$loginSecret = Auth::tokenGenerator();
@@ -741,10 +760,17 @@ App::post('/v1/account/sessions/magic-url')
$url = Template::unParseURL($url);
$from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $project->getAttribute('name'));
$body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
$subject = $locale->getText("emails.magicSession.subject");
$smtpEnabled = $project->getAttribute('smtp', [])['enabled'] ?? false;
$customTemplate = $project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ?? [];
if ($smtpEnabled && !empty($customTemplate)) {
$body = $customTemplate['message'] ?? $body;
$subject = $customTemplate['subject'] ?? $subject;
$from = $customTemplate['senderName'] ?? $from;
}
$body
->setParam('{{subject}}', $subject)
->setParam('{{hello}}', $locale->getText("emails.magicSession.hello"))
@@ -809,27 +835,30 @@ App::put('/v1/account/sessions/magic-url')
->param('secret', '', new Text(256), 'Valid verification token.')
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('project')
->inject('locale')
->inject('geodb')
->inject('events')
->action(function (string $userId, string $secret, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $events) {
->action(function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $events) {
/** @var Utopia\Database\Document $user */
$user = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
$userFromRequest = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
if ($user->isEmpty()) {
if ($userFromRequest->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$token = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_MAGIC_URL, $secret);
$token = Auth::tokenVerify($userFromRequest->getAttribute('tokens', []), Auth::TOKEN_TYPE_MAGIC_URL, $secret);
if (!$token) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
$user->setAttributes($userFromRequest->getArrayCopy());
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
@@ -874,9 +903,9 @@ App::put('/v1/account/sessions/magic-url')
$user->setAttribute('emailVerification', true);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
if (false === $user) {
try {
$dbForProject->updateDocument('users', $user->getId(), $user);
} catch (\Throwable $th) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB');
}
@@ -929,11 +958,13 @@ App::post('/v1/account/sessions/phone')
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->inject('request')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('events')
->inject('messaging')
->action(function (string $userId, string $phone, Request $request, Response $response, Document $project, Database $dbForProject, Event $events, EventPhone $messaging) {
->inject('locale')
->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $events, EventPhone $messaging, Locale $locale) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
@@ -943,9 +974,10 @@ App::post('/v1/account/sessions/phone')
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$user = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]);
if (!$user) {
$result = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]);
if ($result !== false && !$result->isEmpty()) {
$user->setAttributes($result->getArrayCopy());
} else {
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if ($limit !== 0) {
@@ -957,8 +989,7 @@ App::post('/v1/account/sessions/phone')
}
$userId = $userId == 'unique()' ? ID::unique() : $userId;
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
$user->setAttributes([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
@@ -979,7 +1010,9 @@ App::post('/v1/account/sessions/phone')
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $phone])
])));
]);
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
}
$secret = Auth::codeGenerator();
@@ -1007,9 +1040,19 @@ App::post('/v1/account/sessions/phone')
$dbForProject->deleteCachedDocument('users', $user->getId());
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.login-' . $locale->default] ?? [];
if (!empty($customTemplate)) {
$message = $customTemplate['message'] ?? $message;
}
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
$messaging
->setRecipient($phone)
->setMessage($secret)
->setMessage($message)
->trigger();
$events->setPayload(
@@ -1048,25 +1091,28 @@ App::put('/v1/account/sessions/phone')
->param('secret', '', new Text(256), 'Valid verification token.')
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('project')
->inject('locale')
->inject('geodb')
->inject('events')
->action(function (string $userId, string $secret, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $events) {
->action(function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $events) {
$user = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
$userFromRequest = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
if ($user->isEmpty()) {
if ($userFromRequest->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$token = Auth::phoneTokenVerify($user->getAttribute('tokens', []), $secret);
$token = Auth::phoneTokenVerify($userFromRequest->getAttribute('tokens', []), $secret);
if (!$token) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
$user->setAttributes($userFromRequest->getArrayCopy());
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
@@ -1109,7 +1155,7 @@ App::put('/v1/account/sessions/phone')
$user->setAttribute('phoneVerification', true);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$dbForProject->updateDocument('users', $user->getId(), $user);
if (false === $user) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB');
@@ -1194,7 +1240,7 @@ App::post('/v1/account/sessions/anonymous')
}
$userId = ID::unique();
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
$user->setAttributes([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
@@ -1215,8 +1261,9 @@ App::post('/v1/account/sessions/anonymous')
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => $userId
])));
'search' => $userId,
]);
Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
// Create session token
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
@@ -2064,12 +2111,13 @@ App::post('/v1/account/recovery')
->param('url', '', fn ($clients) => new Host($clients), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients'])
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('project')
->inject('locale')
->inject('mails')
->inject('events')
->action(function (string $email, string $url, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Mail $mails, Event $events) {
->action(function (string $email, string $url, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Mail $mails, Event $events) {
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -2089,6 +2137,8 @@ App::post('/v1/account/recovery')
throw new Exception(Exception::USER_NOT_FOUND);
}
$user->setAttributes($profile->getArrayCopy());
if (false === $profile->getAttribute('status')) { // Account is blocked
throw new Exception(Exception::USER_BLOCKED);
}
@@ -2127,6 +2177,14 @@ App::post('/v1/account/recovery')
$body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
$subject = $locale->getText("emails.recovery.subject");
$smtpEnabled = $project->getAttribute('smtp', [])['enabled'] ?? false;
$customTemplate = $project->getAttribute('templates', [])['email.recovery-' . $locale->default] ?? [];
if ($smtpEnabled && !empty($customTemplate)) {
$body = $customTemplate['message'] ?? $body;
$subject = $customTemplate['subject'] ?? $subject;
$from = $customTemplate['senderName'] ?? $from;
}
$body
->setParam('{{subject}}', $subject)
->setParam('{{hello}}', $locale->getText("emails.recovery.hello"))
@@ -2195,10 +2253,11 @@ App::put('/v1/account/recovery')
->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
->param('passwordAgain', '', new Password(), 'Repeat new user password. Must be at least 8 chars.')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('project')
->inject('events')
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Database $dbForProject, Document $project, Event $events) {
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Document $user, Database $dbForProject, Event $events) {
if ($password !== $passwordAgain) {
throw new Exception(Exception::USER_PASSWORD_MISMATCH);
}
@@ -2240,6 +2299,8 @@ App::put('/v1/account/recovery')
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('emailVerification', true));
$user->setAttributes($profile->getArrayCopy());
$recoveryDocument = $dbForProject->getDocument('tokens', $recovery);
/**
@@ -2326,6 +2387,15 @@ App::post('/v1/account/verification')
$from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $projectName);
$body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
$subject = $locale->getText("emails.verification.subject");
$smtpEnabled = $project->getAttribute('smtp', [])['enabled'] ?? false;
$customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? [];
if ($smtpEnabled && !empty($customTemplate)) {
$body = $customTemplate['message'] ?? $body;
$subject = $customTemplate['subject'] ?? $subject;
$from = $customTemplate['senderName'] ?? $from;
}
$body
->setParam('{{subject}}', $subject)
->setParam('{{hello}}', $locale->getText("emails.verification.hello"))
@@ -2412,6 +2482,8 @@ App::put('/v1/account/verification')
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
$user->setAttributes($profile->getArrayCopy());
$verificationDocument = $dbForProject->getDocument('tokens', $verification);
/**
@@ -2453,7 +2525,9 @@ App::post('/v1/account/verification/phone')
->inject('dbForProject')
->inject('events')
->inject('messaging')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events, EventPhone $messaging) {
->inject('project')
->inject('locale')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events, EventPhone $messaging, Document $project, Locale $locale) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED);
@@ -2491,9 +2565,19 @@ App::post('/v1/account/verification/phone')
$dbForProject->deleteCachedDocument('users', $user->getId());
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.verification-' . $locale->default] ?? [];
if (!empty($customTemplate)) {
$message = $customTemplate['message'] ?? $message;
}
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
$messaging
->setRecipient($user->getAttribute('phone'))
->setMessage($secret)
->setMessage($message)
->trigger()
;
@@ -2556,6 +2640,8 @@ App::put('/v1/account/verification/phone')
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));
$user->setAttributes($profile->getArrayCopy());
$verificationDocument = $dbForProject->getDocument('tokens', $verification);
/**
+23 -1
View File
@@ -19,7 +19,7 @@ App::get('/v1/locale')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOCALE)
->label('sdk.offline.model', '/locale')
->label('sdk.offline.model', '/localed')
->label('sdk.offline.key', 'current')
->inject('request')
->inject('response')
@@ -68,6 +68,28 @@ App::get('/v1/locale')
$response->dynamic(new Document($output), Response::MODEL_LOCALE);
});
App::get('/v1/locale/codes')
->desc('List Locale Codes')
->groups(['api', 'locale'])
->label('scope', 'locale.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'locale')
->label('sdk.method', 'listCodes')
->label('sdk.description', '/docs/references/locale/list-locale-codes.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOCALE_CODE_LIST)
->label('sdk.offline.model', '/locale/localeCode')
->label('sdk.offline.key', 'current')
->inject('response')
->action(function (Response $response) {
$codes = Config::getParam('locale-codes');
$response->dynamic(new Document([
'localeCodes' => $codes,
'total' => count($codes),
]), Response::MODEL_LOCALE_CODE_LIST);
});
App::get('/v1/locale/countries')
->desc('List Countries')
->groups(['api', 'locale'])
+334
View File
@@ -28,14 +28,19 @@ use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Registry\Registry;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Utopia\Database\Exception\Duplicate;
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\WhiteList;
use Appwrite\Template\Template;
use Utopia\Locale\Locale;
use PHPMailer\PHPMailer\PHPMailer;
App::init()
->groups(['projects'])
@@ -1705,3 +1710,332 @@ App::delete('/v1/projects/:projectId/domains/:domainId')
$response->noContent();
});
// CUSTOM SMTP and Templates
App::patch('/v1/projects/:projectId/smtp')
->desc('Update SMTP configuration')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateSmtpConfiguration')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->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('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) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
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);
}
$smtp = [
'enabled' => $enabled,
'sender' => $sender,
'host' => $host,
'port' => $port,
'username' => $username,
'password' => $password,
'secure' => $secure,
];
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('smtp', $smtp));
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::get('/v1/projects/:projectId/templates/sms/:type/:locale')
->desc('Get custom SMS template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'getSmsTemplate')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->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'])
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
if (is_null($template)) {
$template = [
'message' => Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl')->render(),
];
}
$template['type'] = $type;
$template['locale'] = $locale;
$response->dynamic(new Document($template), Response::MODEL_SMS_TEMPLATE);
});
App::get('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Get custom email template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'getEmailTemplate')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->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'])
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$template = $templates['email.' . $type . '-' . $locale] ?? null;
$localeObj = new Locale($locale);
if (is_null($template)) {
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
$message = $message
->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello"))
->setParam('{{name}}', '')
->setParam('{{body}}', $localeObj->getText("emails.{$type}.body"))
->setParam('{{footer}}', $localeObj->getText("emails.{$type}.footer"))
->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();
$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
];
}
$template['type'] = $type;
$template['locale'] = $locale;
$response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE);
});
App::patch('/v1/projects/:projectId/templates/sms/:type/:locale')
->desc('Update custom SMS template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateSmsTemplate')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->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('message', '', new Text(0), 'Template message')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$templates['sms.' . $type . '-' . $locale] = [
'message' => $message
];
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
$response->dynamic(new Document([
'message' => $message,
'type' => $type,
'locale' => $locale,
]), Response::MODEL_SMS_TEMPLATE);
});
App::patch('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Update custom email templates')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateEmailTemplate')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->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('subject', '', new Text(255), 'Email Subject')
->param('message', '', new Text(0), 'Template message')
->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) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$templates['email.' . $type . '-' . $locale] = [
'senderName' => $senderName,
'senderEmail' => $senderEmail,
'subject' => $subject,
'replyTo' => $replyTo,
'message' => $message
];
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
$response->dynamic(new Document([
'type' => $type,
'locale' => $locale,
'senderName' => $senderName,
'senderEmail' => $senderEmail,
'subject' => $subject,
'replyTo' => $replyTo,
'message' => $message
]), Response::MODEL_EMAIL_TEMPLATE);
});
App::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
->desc('Reset custom SMS template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'deleteSmsTemplate')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->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'])
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
if (is_null($template)) {
throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION);
}
unset($template['sms.' . $type . '-' . $locale]);
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
$response->dynamic(new Document([
'type' => $type,
'locale' => $locale,
'message' => $template['message']
]), Response::MODEL_SMS_TEMPLATE);
});
App::delete('/v1/projects/:projectId/templates/email/:type/:locale')
->desc('Reset custom email template')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'deleteEmailTemplate')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->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'])
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$templates = $project->getAttribute('templates', []);
$template = $templates['email.' . $type . '-' . $locale] ?? null;
if (is_null($template)) {
throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION);
}
unset($templates['email.' . $type . '-' . $locale]);
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
$response->dynamic(new Document([
'type' => $type,
'locale' => $locale,
'senderName' => $template['senderName'],
'senderEmail' => $template['senderEmail'],
'subject' => $template['subject'],
'replyTo' => $template['replyTo'],
'message' => $template['message']
]), Response::MODEL_EMAIL_TEMPLATE);
});
+22 -3
View File
@@ -547,6 +547,15 @@ App::post('/v1/teams/:teamId/memberships')
$from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $projectName);
$body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
$subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName);
$smtpEnabled = $project->getAttribute('smtp', [])['enabled'] ?? false;
$customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? [];
if ($smtpEnabled && !empty($customTemplate)) {
$body = $customTemplate['message'];
$subject = $customTemplate['subject'];
$from = $customTemplate['senderName'];
}
$body->setParam('{{owner}}', $user->getAttribute('name'));
$body->setParam('{{team}}', $team->getAttribute('name'));
@@ -576,9 +585,19 @@ App::post('/v1/teams/:teamId/memberships')
->trigger()
;
} elseif (!empty($phone)) {
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? [];
if (!empty($customTemplate)) {
$message = $customTemplate['message'];
}
$message = $message->setParam('{{token}}', $url);
$message = $message->render();
$messaging
->setRecipient($phone)
->setMessage($url)
->setMessage($message)
->trigger();
}
}
@@ -848,7 +867,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
}
if ($user->isEmpty()) {
$user = $dbForProject->getDocument('users', $userId); // Get user
$user->setAttributes($dbForProject->getDocument('users', $userId)->getArrayCopy()); // Get user
}
if ($membership->getAttribute('userId') !== $user->getId()) {
@@ -864,7 +883,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->setAttribute('confirm', true)
;
$user = Authorization::skip(fn() => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
Authorization::skip(fn() => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
// Log user in
+3 -2
View File
@@ -50,9 +50,10 @@ App::init()
->inject('dbForConsole')
->inject('user')
->inject('locale')
->inject('localeCodes')
->inject('clients')
->inject('servers')
->action(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients, array $servers) {
->action(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers) {
/*
* Request format
*/
@@ -136,7 +137,7 @@ App::init()
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
if (\in_array($localeParam, Config::getParam('locale-codes'))) {
if (\in_array($localeParam, $localeCodes)) {
$locale->setDefault($localeParam);
}
+37 -11
View File
@@ -5,6 +5,7 @@ use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Usage\Stats;
@@ -101,7 +102,8 @@ App::init()
->inject('database')
->inject('dbForProject')
->inject('mode')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, Database $dbForProject, string $mode) use ($databaseListener) {
->inject('mails')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, Database $dbForProject, string $mode, Mail $mails) use ($databaseListener) {
$route = $utopia->match($request);
@@ -167,9 +169,9 @@ App::init()
}
}
/*
* Background Jobs
*/
/*
* Background Jobs
*/
$events
->setEvent($route->getLabel('event', ''))
->setProject($project)
@@ -191,6 +193,17 @@ App::init()
->setParam('project.{scope}.network.inbound', 0)
->setParam('project.{scope}.network.outbound', 0);
$smtp = $project->getAttribute('smtp', []);
if (!empty($smtp) && ($smtp['enabled'] ?? false)) {
$mails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? 25)
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSenderEmail($smtp['sender'] ?? '')
->setSmtpReplyTo($smtp['replyTo'] ?? '');
}
$deletes->setProject($project);
$database->setProject($project);
@@ -356,6 +369,7 @@ App::shutdown()
->inject('request')
->inject('response')
->inject('project')
->inject('user')
->inject('events')
->inject('audits')
->inject('usage')
@@ -363,8 +377,8 @@ App::shutdown()
->inject('database')
->inject('mode')
->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) use ($parseLabel) {
->inject('dbForConsole')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject, Database $dbForConsole) use ($parseLabel) {
$responsePayload = $response->getPayload();
if (!empty($events->getEvent())) {
@@ -424,7 +438,6 @@ App::shutdown()
$route = $utopia->match($request);
$requestParams = $route->getParamsValues();
$user = $audits->getUser();
/**
* Audit labels
@@ -437,10 +450,7 @@ App::shutdown()
}
}
$pattern = $route->getLabel('audits.userId', null);
if (!empty($pattern)) {
$userId = $parseLabel($pattern, $responsePayload, $requestParams, $user);
$user = $dbForProject->getDocument('users', $userId);
if (!$user->isEmpty()) {
$audits->setUser($user);
}
@@ -551,6 +561,22 @@ App::shutdown()
->setParam('project.{scope}.network.outbound', $response->getSize())
->submit();
}
/**
* Update user last activity
*/
if (!$user->isEmpty()) {
$accessedAt = $user->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());
if (APP_MODE_ADMIN !== $mode) {
$dbForProject->updateDocument('users', $user->getId(), $user);
} else {
$dbForConsole->updateDocument('users', $user->getId(), $user);
}
}
}
});
App::init()
+25 -72
View File
@@ -47,6 +47,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Logger\Logger;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
@@ -99,6 +100,7 @@ const APP_LIMIT_WRITE_RATE_DEFAULT = 60; // Default maximum write rate per rate
const APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT = 60; // Default maximum write rate period in seconds
const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return in list API calls
const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours
const APP_USER_ACCCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 506;
const APP_VERSION_STABLE = '1.3.7';
@@ -205,6 +207,7 @@ Config::load('locale-languages', __DIR__ . '/config/locale/languages.php');
Config::load('locale-phones', __DIR__ . '/config/locale/phones.php');
Config::load('locale-countries', __DIR__ . '/config/locale/countries.php');
Config::load('locale-continents', __DIR__ . '/config/locale/continents.php');
Config::load('locale-templates', __DIR__ . '/config/locale/templates.php');
Config::load('storage-logos', __DIR__ . '/config/storage/logos.php');
Config::load('storage-mimes', __DIR__ . '/config/storage/mimes.php');
Config::load('storage-inputs', __DIR__ . '/config/storage/inputs.php');
@@ -683,78 +686,24 @@ $register->set('promiseAdapter', function () {
* Localization
*/
Locale::$exceptions = false;
Locale::setLanguageFromJSON('af', __DIR__ . '/config/locale/translations/af.json');
Locale::setLanguageFromJSON('ar', __DIR__ . '/config/locale/translations/ar.json');
Locale::setLanguageFromJSON('as', __DIR__ . '/config/locale/translations/as.json');
Locale::setLanguageFromJSON('az', __DIR__ . '/config/locale/translations/az.json');
Locale::setLanguageFromJSON('be', __DIR__ . '/config/locale/translations/be.json');
Locale::setLanguageFromJSON('bg', __DIR__ . '/config/locale/translations/bg.json');
Locale::setLanguageFromJSON('bh', __DIR__ . '/config/locale/translations/bh.json');
Locale::setLanguageFromJSON('bn', __DIR__ . '/config/locale/translations/bn.json');
Locale::setLanguageFromJSON('bs', __DIR__ . '/config/locale/translations/bs.json');
Locale::setLanguageFromJSON('ca', __DIR__ . '/config/locale/translations/ca.json');
Locale::setLanguageFromJSON('cs', __DIR__ . '/config/locale/translations/cs.json');
Locale::setLanguageFromJSON('da', __DIR__ . '/config/locale/translations/da.json');
Locale::setLanguageFromJSON('de', __DIR__ . '/config/locale/translations/de.json');
Locale::setLanguageFromJSON('el', __DIR__ . '/config/locale/translations/el.json');
Locale::setLanguageFromJSON('en', __DIR__ . '/config/locale/translations/en.json');
Locale::setLanguageFromJSON('eo', __DIR__ . '/config/locale/translations/eo.json');
Locale::setLanguageFromJSON('es', __DIR__ . '/config/locale/translations/es.json');
Locale::setLanguageFromJSON('fa', __DIR__ . '/config/locale/translations/fa.json');
Locale::setLanguageFromJSON('fi', __DIR__ . '/config/locale/translations/fi.json');
Locale::setLanguageFromJSON('fo', __DIR__ . '/config/locale/translations/fo.json');
Locale::setLanguageFromJSON('fr', __DIR__ . '/config/locale/translations/fr.json');
Locale::setLanguageFromJSON('ga', __DIR__ . '/config/locale/translations/ga.json');
Locale::setLanguageFromJSON('gu', __DIR__ . '/config/locale/translations/gu.json');
Locale::setLanguageFromJSON('he', __DIR__ . '/config/locale/translations/he.json');
Locale::setLanguageFromJSON('hi', __DIR__ . '/config/locale/translations/hi.json');
Locale::setLanguageFromJSON('hr', __DIR__ . '/config/locale/translations/hr.json');
Locale::setLanguageFromJSON('hu', __DIR__ . '/config/locale/translations/hu.json');
Locale::setLanguageFromJSON('hy', __DIR__ . '/config/locale/translations/hy.json');
Locale::setLanguageFromJSON('id', __DIR__ . '/config/locale/translations/id.json');
Locale::setLanguageFromJSON('is', __DIR__ . '/config/locale/translations/is.json');
Locale::setLanguageFromJSON('it', __DIR__ . '/config/locale/translations/it.json');
Locale::setLanguageFromJSON('ja', __DIR__ . '/config/locale/translations/ja.json');
Locale::setLanguageFromJSON('jv', __DIR__ . '/config/locale/translations/jv.json');
Locale::setLanguageFromJSON('kn', __DIR__ . '/config/locale/translations/kn.json');
Locale::setLanguageFromJSON('km', __DIR__ . '/config/locale/translations/km.json');
Locale::setLanguageFromJSON('ko', __DIR__ . '/config/locale/translations/ko.json');
Locale::setLanguageFromJSON('la', __DIR__ . '/config/locale/translations/la.json');
Locale::setLanguageFromJSON('lb', __DIR__ . '/config/locale/translations/lb.json');
Locale::setLanguageFromJSON('lt', __DIR__ . '/config/locale/translations/lt.json');
Locale::setLanguageFromJSON('lv', __DIR__ . '/config/locale/translations/lv.json');
Locale::setLanguageFromJSON('ml', __DIR__ . '/config/locale/translations/ml.json');
Locale::setLanguageFromJSON('mr', __DIR__ . '/config/locale/translations/mr.json');
Locale::setLanguageFromJSON('ms', __DIR__ . '/config/locale/translations/ms.json');
Locale::setLanguageFromJSON('nb', __DIR__ . '/config/locale/translations/nb.json');
Locale::setLanguageFromJSON('ne', __DIR__ . '/config/locale/translations/ne.json');
Locale::setLanguageFromJSON('nl', __DIR__ . '/config/locale/translations/nl.json');
Locale::setLanguageFromJSON('nn', __DIR__ . '/config/locale/translations/nn.json');
Locale::setLanguageFromJSON('or', __DIR__ . '/config/locale/translations/or.json');
Locale::setLanguageFromJSON('pa', __DIR__ . '/config/locale/translations/pa.json');
Locale::setLanguageFromJSON('pl', __DIR__ . '/config/locale/translations/pl.json');
Locale::setLanguageFromJSON('pt-br', __DIR__ . '/config/locale/translations/pt-br.json');
Locale::setLanguageFromJSON('pt-pt', __DIR__ . '/config/locale/translations/pt-pt.json');
Locale::setLanguageFromJSON('ro', __DIR__ . '/config/locale/translations/ro.json');
Locale::setLanguageFromJSON('ru', __DIR__ . '/config/locale/translations/ru.json');
Locale::setLanguageFromJSON('sa', __DIR__ . '/config/locale/translations/sa.json');
Locale::setLanguageFromJSON('sd', __DIR__ . '/config/locale/translations/sd.json');
Locale::setLanguageFromJSON('si', __DIR__ . '/config/locale/translations/si.json');
Locale::setLanguageFromJSON('sk', __DIR__ . '/config/locale/translations/sk.json');
Locale::setLanguageFromJSON('sl', __DIR__ . '/config/locale/translations/sl.json');
Locale::setLanguageFromJSON('sn', __DIR__ . '/config/locale/translations/sn.json');
Locale::setLanguageFromJSON('sq', __DIR__ . '/config/locale/translations/sq.json');
Locale::setLanguageFromJSON('sv', __DIR__ . '/config/locale/translations/sv.json');
Locale::setLanguageFromJSON('ta', __DIR__ . '/config/locale/translations/ta.json');
Locale::setLanguageFromJSON('te', __DIR__ . '/config/locale/translations/te.json');
Locale::setLanguageFromJSON('th', __DIR__ . '/config/locale/translations/th.json');
Locale::setLanguageFromJSON('tl', __DIR__ . '/config/locale/translations/tl.json');
Locale::setLanguageFromJSON('tr', __DIR__ . '/config/locale/translations/tr.json');
Locale::setLanguageFromJSON('uk', __DIR__ . '/config/locale/translations/uk.json');
Locale::setLanguageFromJSON('ur', __DIR__ . '/config/locale/translations/ur.json');
Locale::setLanguageFromJSON('vi', __DIR__ . '/config/locale/translations/vi.json');
Locale::setLanguageFromJSON('zh-cn', __DIR__ . '/config/locale/translations/zh-cn.json');
Locale::setLanguageFromJSON('zh-tw', __DIR__ . '/config/locale/translations/zh-tw.json');
$locales = Config::getParam('locale-codes', []);
foreach ($locales as $locale) {
$code = $locale['code'];
$path = __DIR__ . '/config/locale/translations/' . $code . '.json';
if (!\file_exists($path)) {
$path = __DIR__ . '/config/locale/translations/' . \substr($code, 0, 2) . '.json'; // if `ar-ae` doesn't exist, look for `ar`
if (!\file_exists($path)) {
var_dump('Unable to find tralsnations for ' . $locale['code'] . ' so using en.json');
$path = __DIR__ . '/config/locale/translations/en.json'; // if none translation exists, use default from `en.json`
}
}
Locale::setLanguageFromJSON($code, $path);
}
\stream_context_set_default([ // Set global user agent and http settings
'http' => [
@@ -781,6 +730,10 @@ App::setResource('register', fn() => $register);
App::setResource('locale', fn() => new Locale(App::getEnv('_APP_LOCALE', 'en')));
App::setResource('localeCodes', function () {
return array_map(fn($locale) => $locale['code'], Config::getParam('locale-codes', []));
});
// Queues
App::setResource('events', fn() => new Event('', ''));
App::setResource('audits', fn() => new Audit());
+39 -13
View File
@@ -3,6 +3,7 @@
use Appwrite\Resque\Worker;
use Utopia\App;
use Utopia\CLI\Console;
use PHPMailer\PHPMailer\PHPMailer;
require_once __DIR__ . '/../init.php';
@@ -24,8 +25,10 @@ class MailsV1 extends Worker
{
global $register;
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
Console::info('Skipped mail processing. No SMTP server hostname has been set.');
$smtp = $this->args['smtp'];
if (empty($smtp) && empty(App::getEnv('_APP_SMTP_HOST'))) {
Console::info('Skipped mail processing. No SMTP configuration has been set.');
return;
}
@@ -37,17 +40,7 @@ class MailsV1 extends Worker
$from = $this->args['from'];
/** @var \PHPMailer\PHPMailer\PHPMailer $mail */
$mail = $register->get('smtp');
// Set project mail
/*$register->get('smtp')
->setFrom(
App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM),
($project->getId() === 'console')
? \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server'))
: \sprintf(Locale::getText('account.emails.team'), $project->getAttribute('name')
)
);*/
$mail = empty($smtp) ? $register->get('smtp') : $this->getMailer($smtp);
$mail->clearAddresses();
$mail->clearAllRecipients();
@@ -58,6 +51,9 @@ class MailsV1 extends Worker
$mail->setFrom(App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), (empty($from) ? \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')) : $from));
$mail->addAddress($recipient, $name);
if (isset($smtp['replyTo'])) {
$mail->addReplyTo($smtp['replyTo']);
}
$mail->Subject = $subject;
$mail->Body = $body;
$mail->AltBody = \strip_tags($body);
@@ -69,6 +65,36 @@ class MailsV1 extends Worker
}
}
protected function getMailer(array $smtp): PHPMailer
{
$mail = new PHPMailer(true);
$mail->isSMTP();
$username = $smtp['username'];
$password = $smtp['password'];
$mail->XMailer = 'Appwrite Mailer';
$mail->Host = $smtp['host'];
$mail->Port = $smtp['port'];
$mail->SMTPAuth = (!empty($username) && !empty($password));
$mail->Username = $username;
$mail->Password = $password;
$mail->SMTPSecure = $smtp['secure'] === 'tls';
$mail->SMTPAutoTLS = false;
$mail->CharSet = 'UTF-8';
$from = \urldecode($smtp['senderName'] ?? App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$email = $smtp['senderEmail'] ?? App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$mail->setFrom($email, $from);
$mail->addReplyTo($email, $from);
$mail->isHTML(true);
return $mail;
}
public function shutdown(): void
{
}
+3 -3
View File
@@ -43,13 +43,13 @@
"ext-sockets": "*",
"appwrite/php-clamav": "1.1.*",
"appwrite/php-runtimes": "0.11.*",
"utopia-php/abuse": "0.26.*",
"utopia-php/abuse": "0.27.*",
"utopia-php/analytics": "0.2.*",
"utopia-php/audit": "0.28.*",
"utopia-php/audit": "0.29.*",
"utopia-php/cache": "0.8.*",
"utopia-php/cli": "0.13.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.37.*",
"utopia-php/database": "0.38.*",
"utopia-php/domains": "1.1.*",
"utopia-php/framework": "0.28.*",
"utopia-php/image": "0.5.*",
Generated
+21 -21
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0f20fb41e9b250b6763af1b734bb8d2d",
"content-hash": "0299a46dacaa5ae6607931f91b459db4",
"packages": [
{
"name": "adhocore/jwt",
@@ -1802,23 +1802,23 @@
},
{
"name": "utopia-php/abuse",
"version": "0.26.0",
"version": "0.27.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "fb73180f0588bc8826b85d433393b983bdc37cfa"
"reference": "d1115f5843e903ffaba9c23e450b33c0fe265ae0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/fb73180f0588bc8826b85d433393b983bdc37cfa",
"reference": "fb73180f0588bc8826b85d433393b983bdc37cfa",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/d1115f5843e903ffaba9c23e450b33c0fe265ae0",
"reference": "d1115f5843e903ffaba9c23e450b33c0fe265ae0",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.37.*"
"utopia-php/database": "0.38.*"
},
"require-dev": {
"laravel/pint": "1.5.*",
@@ -1845,9 +1845,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/0.26.0"
"source": "https://github.com/utopia-php/abuse/tree/0.27.0"
},
"time": "2023-06-15T00:53:36+00:00"
"time": "2023-07-15T00:53:50+00:00"
},
{
"name": "utopia-php/analytics",
@@ -1906,21 +1906,21 @@
},
{
"name": "utopia-php/audit",
"version": "0.28.0",
"version": "0.29.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "abf4124bec20b6ab3555869b64afe5b274e37165"
"reference": "5318538f457bf73623629345c98ea06371ca5dd4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/abf4124bec20b6ab3555869b64afe5b274e37165",
"reference": "abf4124bec20b6ab3555869b64afe5b274e37165",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/5318538f457bf73623629345c98ea06371ca5dd4",
"reference": "5318538f457bf73623629345c98ea06371ca5dd4",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/database": "0.37.*"
"utopia-php/database": "0.38.*"
},
"require-dev": {
"laravel/pint": "1.5.*",
@@ -1947,9 +1947,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.28.0"
"source": "https://github.com/utopia-php/audit/tree/0.29.0"
},
"time": "2023-06-15T00:52:49+00:00"
"time": "2023-07-15T00:51:10+00:00"
},
{
"name": "utopia-php/cache",
@@ -2106,16 +2106,16 @@
},
{
"name": "utopia-php/database",
"version": "0.37.1",
"version": "0.38.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "4035d3f7e3393385eabc7816055047659c6fb4d3"
"reference": "59e4684cf87e03c12dab9240158c1dfc6888e534"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/4035d3f7e3393385eabc7816055047659c6fb4d3",
"reference": "4035d3f7e3393385eabc7816055047659c6fb4d3",
"url": "https://api.github.com/repos/utopia-php/database/zipball/59e4684cf87e03c12dab9240158c1dfc6888e534",
"reference": "59e4684cf87e03c12dab9240158c1dfc6888e534",
"shasum": ""
},
"require": {
@@ -2156,9 +2156,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.37.1"
"source": "https://github.com/utopia-php/database/tree/0.38.0"
},
"time": "2023-06-15T06:36:27+00:00"
"time": "2023-07-14T07:49:38+00:00"
},
{
"name": "utopia-php/domains",
@@ -0,0 +1 @@
List of all locale codes in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
+135 -1
View File
@@ -12,6 +12,7 @@ class Mail extends Event
protected string $name = '';
protected string $subject = '';
protected string $body = '';
protected array $smtp = [];
public function __construct()
{
@@ -32,7 +33,7 @@ class Mail extends Event
}
/**
* Returns set team for the mail event.
* Returns subject for the mail event.
*
* @return string
*/
@@ -133,6 +134,138 @@ class Mail extends Event
return $this->name;
}
/**
* Set SMTP Host
*
* @param string $host
* @return self
*/
public function setSmtpHost(string $host): self
{
$this->smtp['host'] = $host;
return $this;
}
/**
* Set SMTP port
*
* @param int port
* @return self
*/
public function setSmtpPort(int $port): self
{
$this->smtp['port'] = $port;
return $this;
}
/**
* Set SMTP username
*
* @param string $username
* @return self
*/
public function setSmtpUsername(string $username): self
{
$this->smtp['username'];
return $this;
}
/**
* Set SMTP password
*
* @param string $password
* @return self
*/
public function setSmtpPassword(string $password): self
{
$this->smtp['password'];
return $this;
}
/**
* Set SMTP sender email
*
* @param string $senderEmail
* @return self
*/
public function setSmtpSenderEmail(string $senderEmail): self
{
$this->smtp['senderEmail'] = $senderEmail;
return $this;
}
/**
* Set SMTP reply to
*
* @param string $replyTo
* @return self
*/
public function setSmtpReplyTo(string $replyTo): self
{
$this->smtp['replyTo'] = $replyTo;
return $this;
}
/**
* Get SMTP
*
* @return string
*/
public function getSmtpHost(): string
{
return $this->smtp['host'] ?? '';
}
/**
* Get SMTP port
*
* @return integer
*/
public function getSmtpPort(): int
{
return $this->smtp['port'] ?? 0;
}
/**
* Get SMTP username
*
* @return string
*/
public function getSmtpUsername(): string
{
return $this->smtp['username'] ?? '';
}
/**
* Get SMTP password
*
* @return string
*/
public function getSmtpPassword(): string
{
return $this->smtp['password'] ?? '';
}
/**
* Get SMTP sender email
*
* @return string
*/
public function getSmtpSenderEmail(): string
{
return $this->smtp['senderEmail'] ?? '';
}
/**
* Get SMTP reply to
*
* @return string
*/
public function getSmtpReplyTo(): string
{
return $this->smtp['replyTo'] ?? '';
}
/**
* Executes the event and sends it to the mails worker.
*
@@ -147,6 +280,7 @@ class Mail extends Event
'name' => $this->name,
'subject' => $this->subject,
'body' => $this->body,
'smtp' => $this->smtp,
'events' => Event::generateEvents($this->getEvent(), $this->getParams())
]);
}
+4
View File
@@ -170,6 +170,10 @@ class Exception extends \Exception
public const PROJECT_RESERVED_PROJECT = 'project_reserved_project';
public const PROJECT_KEY_EXPIRED = 'project_key_expired';
public const PROJECT_SMTP_CONFIG_INVALID = 'project_smtp_config_invalid';
public const PROJECT_TEMPLATE_DEFAULT_DELETION = 'project_template_default_deletion';
/** Webhooks */
public const WEBHOOK_NOT_FOUND = 'webhook_not_found';
+24
View File
@@ -2,6 +2,8 @@
namespace Appwrite\Migration\Version;
use Appwrite\Auth\Auth;
use Utopia\Config\Config;
use Appwrite\Migration\Migration;
use Utopia\CLI\Console;
use Utopia\Database\Database;
@@ -76,6 +78,24 @@ class V19 extends Migration
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
switch ($id) {
case 'projects':
try {
/**
* Create 'passwordHistory' attribute
*/
$this->createAttributeFromCollection($this->projectDB, $id, 'smtp');
$this->createAttributeFromCollection($this->projectDB, $id, 'templates');
$this->projectDB->deleteCachedCollection($id);
} catch (\Throwable $th) {
Console::warning("'SMTP and Templates' from {$id}: {$th->getMessage()}");
}
break;
default:
break;
}
if (!in_array($id, ['files', 'collections'])) {
$this->alterPermissionIndex($id);
}
@@ -98,6 +118,10 @@ class V19 extends Migration
* Bump version number.
*/
$document->setAttribute('version', '1.4.0');
$document->setAttribute('smtp', []);
$document->setAttribute('templates', []);
break;
}
+19 -8
View File
@@ -64,6 +64,7 @@ use Appwrite\Utopia\Response\Model\Platform;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Rule;
use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Utopia\Response\Model\TemplateEmail;
use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Webhook;
use Appwrite\Utopia\Response\Model\Preferences;
@@ -72,9 +73,11 @@ use Appwrite\Utopia\Response\Model\HealthQueue;
use Appwrite\Utopia\Response\Model\HealthStatus;
use Appwrite\Utopia\Response\Model\HealthTime;
use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\LocaleCode;
use Appwrite\Utopia\Response\Model\Mock; // Keep last
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
use Appwrite\Utopia\Response\Model\UsageDatabase;
@@ -163,6 +166,8 @@ class Response extends SwooleResponse
// Locale
public const MODEL_LOCALE = 'locale';
public const MODEL_LOCALE_CODE = 'localeCode';
public const MODEL_LOCALE_CODE_LIST = 'localeCodeList';
public const MODEL_COUNTRY = 'country';
public const MODEL_COUNTRY_LIST = 'countryList';
public const MODEL_CONTINENT = 'continent';
@@ -208,6 +213,8 @@ class Response extends SwooleResponse
public const MODEL_DOMAIN_LIST = 'domainList';
public const MODEL_VARIABLE = 'variable';
public const MODEL_VARIABLE_LIST = 'variableList';
public const MODEL_SMS_TEMPLATE = 'smsTemplate';
public const MODEL_EMAIL_TEMPLATE = 'emailTemplate';
// Health
public const MODEL_HEALTH_STATUS = 'healthStatus';
@@ -280,6 +287,7 @@ class Response extends SwooleResponse
->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE))
->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false))
->setModel(new BaseList('Variables List', self::MODEL_VARIABLE_LIST, 'variables', self::MODEL_VARIABLE))
->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE))
// Entities
->setModel(new Database())
->setModel(new Collection())
@@ -312,6 +320,7 @@ class Response extends SwooleResponse
->setModel(new Token())
->setModel(new JWT())
->setModel(new Locale())
->setModel(new LocaleCode())
->setModel(new File())
->setModel(new Bucket())
->setModel(new Team())
@@ -348,6 +357,8 @@ class Response extends SwooleResponse
->setModel(new UsageFunctions())
->setModel(new UsageFunction())
->setModel(new UsageProject())
->setModel(new TemplateSMS())
->setModel(new TemplateEmail())
->setModel(new ConsoleVariables())
// Verification
// Recovery
@@ -418,7 +429,7 @@ class Response extends SwooleResponse
*/
public function dynamic(Document $document, string $model): void
{
$output = $this->output($document, $model);
$output = $this->output(new Document($document->getArrayCopy()), $model);
// If filter is set, parse the output
if (self::hasFilter()) {
@@ -459,14 +470,14 @@ class Response extends SwooleResponse
*/
public function output(Document $document, string $model): array
{
$data = $document;
$data = new Document($document->getArrayCopy());
$model = $this->getModel($model);
$output = [];
$document = $model->filter($document);
$data = $model->filter($document);
if ($model->isAny()) {
$this->payload = $document->getArrayCopy();
$this->payload = $data->getArrayCopy();
return $this->payload;
}
@@ -474,18 +485,18 @@ class Response extends SwooleResponse
foreach ($model->getRules() as $key => $rule) {
if (!$document->isSet($key) && $rule['required']) { // do not set attribute in response if not required
if (\array_key_exists('default', $rule)) {
$document->setAttribute($key, $rule['default']);
$data->setAttribute($key, $rule['default']);
} else {
throw new Exception('Model ' . $model->getName() . ' is missing response key: ' . $key);
}
}
if ($rule['array']) {
if (!is_array($data[$key])) {
if (!is_array($document[$key])) {
throw new Exception($key . ' must be an array of type ' . $rule['type']);
}
foreach ($data[$key] as $index => $item) {
foreach ($document[$key] as $index => $item) {
if ($item instanceof Document) {
if (\is_array($rule['type'])) {
foreach ($rule['type'] as $type) {
@@ -513,7 +524,7 @@ class Response extends SwooleResponse
}
}
} else {
if ($data[$key] instanceof Document) {
if ($document[$key] instanceof Document) {
$data[$key] = $this->output($data[$key], $rule['type']);
}
}
@@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class LocaleCode extends Model
{
public function __construct()
{
$this
->addRule('code', [
'type' => self::TYPE_STRING,
'description' => 'Locale codes in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)',
'default' => '',
'example' => 'en-us',
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Locale name',
'default' => '',
'example' => 'US',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'LocaleCode';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_LOCALE_CODE;
}
}
@@ -173,6 +173,49 @@ class Project extends Model
'example' => new \stdClass(),
'array' => true,
])
->addRule('smtpEnabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Status for custom SMTP',
'default' => false,
'example' => false,
'array' => false
])
->addRule('smtpSender', [
'type' => self::TYPE_STRING,
'description' => 'SMTP sender email',
'default' => '',
'example' => 'john@appwrite.io',
])
->addRule('smtpHost', [
'type' => self::TYPE_STRING,
'description' => 'SMTP server host name',
'default' => '',
'example' => 'mail.appwrite.io',
])
->addRule('smtpPort', [
'type' => self::TYPE_INTEGER,
'description' => 'SMTP server port',
'default' => '',
'example' => 25,
])
->addRule('smtpUsername', [
'type' => self::TYPE_STRING,
'description' => 'SMTP server username',
'default' => '',
'example' => 'emailuser',
])
->addRule('smtpPassword', [
'type' => self::TYPE_STRING,
'description' => 'SMTP server password',
'default' => '',
'example' => 'securepassword',
])
->addRule('smtpSecure', [
'type' => self::TYPE_STRING,
'description' => 'SMTP server secure protocol',
'default' => '',
'example' => 'tls',
])
;
$services = Config::getParam('services', []);
@@ -238,6 +281,16 @@ class Project extends Model
*/
public function filter(Document $document): Document
{
// SMTP
$smtp = $document->getAttribute('smtp', []);
$document->setAttribute('smtpEnabled', $smtp['enabled'] ?? false);
$document->setAttribute('smtpSender', $smtp['sender'] ?? '');
$document->setAttribute('smtpHost', $smtp['host'] ?? '');
$document->setAttribute('smtpPort', $smtp['port'] ?? '');
$document->setAttribute('smtpUsername', $smtp['username'] ?? '');
$document->setAttribute('smtpPassword', $smtp['password'] ?? '');
$document->setAttribute('smtpSecure', $smtp['secure'] ?? '');
// Services
$values = $document->getAttribute('services', []);
$services = Config::getParam('services', []);
@@ -0,0 +1,32 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response\Model;
abstract class Template extends Model
{
public function __construct()
{
$this
->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Template type',
'default' => '',
'example' => 'verification',
])
->addRule('locale', [
'type' => self::TYPE_STRING,
'description' => 'Template locale',
'default' => '',
'example' => 'en_us',
])
->addRule('message', [
'type' => self::TYPE_STRING,
'description' => 'Template message',
'default' => '',
'example' => 'Click on the link to verify your account.',
])
;
}
}
@@ -0,0 +1,59 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class TemplateEmail extends Template
{
public function __construct()
{
parent::__construct();
$this
->addRule('senderName', [
'type' => self::TYPE_STRING,
'description' => 'Name of the sender',
'default' => '',
'example' => 'My User',
])
->addRule('senderEmail', [
'type' => self::TYPE_STRING,
'description' => 'Email of the sender',
'default' => '',
'example' => 'mail@appwrite.io',
])
->addRule('replyTo', [
'type' => self::TYPE_STRING,
'description' => 'Reply to email address',
'default' => '',
'example' => 'emails@appwrite.io',
])
->addRule('subject', [
'type' => self::TYPE_STRING,
'description' => 'Email subject',
'default' => '',
'example' => 'Please verify your email address',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'EmailTemplate';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_EMAIL_TEMPLATE;
}
}
@@ -0,0 +1,32 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class TemplateSMS extends Template
{
public function __construct()
{
parent::__construct();
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'SmsTemplate';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_SMS_TEMPLATE;
}
}
@@ -120,6 +120,12 @@ class User extends Model
'default' => new \stdClass(),
'example' => ['theme' => 'pink', 'timezone' => 'UTC'],
])
->addRule('accessedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Most recent access date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
;
}
@@ -40,6 +40,8 @@ trait AccountBase
$this->assertEquals($response['body']['email'], $email);
$this->assertEquals($response['body']['name'], $name);
$this->assertEquals($response['body']['labels'], []);
$this->assertArrayHasKey('accessedAt', $response['body']);
$this->assertNotEmpty($response['body']['accessedAt']);
/**
* Test for FAILURE
@@ -127,6 +129,21 @@ trait AccountBase
$sessionId = $response['body']['$id'];
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
// apiKey is only available in custom client test
$apiKey = $this->getProject()['apiKey'];
if (!empty($apiKey)) {
$userId = $response['body']['userId'];
$response = $this->client->call(Client::METHOD_GET, '/users/' . $userId, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $apiKey,
]));
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertArrayHasKey('accessedAt', $response['body']);
$this->assertNotEmpty($response['body']['accessedAt']);
}
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
@@ -207,6 +224,8 @@ trait AccountBase
$this->assertEquals(true, $dateValidator->isValid($response['body']['registration']));
$this->assertEquals($response['body']['email'], $email);
$this->assertEquals($response['body']['name'], $name);
$this->assertArrayHasKey('accessedAt', $response['body']);
$this->assertNotEmpty($response['body']['accessedAt']);
/**
* Test for FAILURE
@@ -369,6 +369,20 @@ class AccountCustomClientTest extends Scope
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
\usleep(1000 * 30); // wait for 30ms to let the shutdown update accessedAt
$apiKey = $this->getProject()['apiKey'];
$userId = $response['body']['userId'];
$response = $this->client->call(Client::METHOD_GET, '/users/' . $userId, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $apiKey,
]));
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertArrayHasKey('accessedAt', $response['body']);
$this->assertNotEmpty($response['body']['accessedAt']);
/**
* Test for FAILURE
*/
+4 -12
View File
@@ -235,7 +235,7 @@ trait LocaleBase
$response = $this->client->call(Client::METHOD_GET, '/locale/countries', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-locale' => $lang,
'x-appwrite-locale' => $lang['code'],
]);
if (!\is_array($response['body']['countries'])) {
@@ -243,30 +243,22 @@ trait LocaleBase
}
foreach ($response['body']['countries'] as $i => $code) {
$this->assertContains($code['code'], $defaultCountries, $code['code'] . ' country should be removed from ' . $lang);
$this->assertContains($code['code'], $defaultCountries, $code['code'] . ' country should be removed from ' . $lang['code']);
}
// foreach (array_keys($defaultCountries) as $i => $code) {
// $this->assertArrayHasKey($code, $response['body']['countries'], $code . ' country is missing from ' . $lang . ' (total: ' . count($response['body']['countries']) . ')');
// }
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(194, $response['body']['total']);
$response = $this->client->call(Client::METHOD_GET, '/locale/continents', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-locale' => $lang,
'x-appwrite-locale' => $lang['code'],
]);
foreach ($response['body']['continents'] as $i => $code) {
$this->assertContains($code['code'], $defaultContinents, $code['code'] . ' continent should be removed from ' . $lang);
$this->assertContains($code['code'], $defaultContinents, $code['code'] . ' continent should be removed from ' . $lang['code']);
}
// foreach (array_keys($defaultContinents) as $i => $code) {
// $this->assertArrayHasKey($code, $response['body']['continents'], $code . ' continent is missing from ' . $lang . ' (total: ' . count($response['body']['continents']) . ')');
// }
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(7, $response['body']['total']);
}
@@ -19,7 +19,9 @@ class ProjectsConsoleClientTest extends Scope
use ProjectConsole;
use SideClient;
/** @group projectsCRUD */
/**
* @group smtpAndTemplates
* @group projectsCRUD */
public function testCreateProject(): array
{
/**
@@ -529,6 +531,156 @@ class ProjectsConsoleClientTest extends Scope
return ['projectId' => $projectId];
}
/**
* @group smtpAndTemplates
* @depends testCreateProject
*/
public function testUpdateProjectSMTP($data): array
{
$id = $data['projectId'];
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'enabled' => true,
'sender' => 'mailer@appwrite.io',
'host' => 'maildev',
'port' => 1025,
'username' => 'user',
'password' => 'password',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertTrue($response['body']['smtpEnabled']);
$this->assertEquals('mailer@appwrite.io', $response['body']['smtpSender']);
$this->assertEquals('maildev', $response['body']['smtpHost']);
$this->assertEquals(1025, $response['body']['smtpPort']);
$this->assertEquals('user', $response['body']['smtpUsername']);
$this->assertEquals('password', $response['body']['smtpPassword']);
$this->assertEquals('', $response['body']['smtpSecure']);
/** Test Reading Project */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertTrue($response['body']['smtpEnabled']);
$this->assertEquals('mailer@appwrite.io', $response['body']['smtpSender']);
$this->assertEquals('maildev', $response['body']['smtpHost']);
$this->assertEquals(1025, $response['body']['smtpPort']);
$this->assertEquals('user', $response['body']['smtpUsername']);
$this->assertEquals('password', $response['body']['smtpPassword']);
$this->assertEquals('', $response['body']['smtpSecure']);
/** Reset */
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'enabled' => false,
'sender' => 'mailer@appwrite.io',
'host' => 'maildev',
'port' => 1025,
'username' => 'user',
'password' => 'password',
]);
return $data;
}
/**
* @group smtpAndTemplates
* @depends testCreateProject */
public function testUpdateTemplates($data): array
{
$id = $data['projectId'];
/** Get Default Email Template */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('Account Verification', $response['body']['subject']);
$this->assertEquals('Project Test 2 Team', $response['body']['senderName']);
$this->assertEquals('team@appwrite.io', $response['body']['senderEmail']);
$this->assertEquals('verification', $response['body']['type']);
$this->assertEquals('en-us', $response['body']['locale']);
$this->assertMatchesRegularExpression('/<!doctype html>/', $response['body']['message']);
/** Update Email template */
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'subject' => 'Please verify your email',
'message' => 'Please verify your email {{url}}',
'senderName' => 'Appwrite Custom',
'senderEmail' => 'custom@appwrite.io',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('Please verify your email', $response['body']['subject']);
$this->assertEquals('Appwrite Custom', $response['body']['senderName']);
$this->assertEquals('custom@appwrite.io', $response['body']['senderEmail']);
$this->assertEquals('verification', $response['body']['type']);
$this->assertEquals('en-us', $response['body']['locale']);
$this->assertEquals('Please verify your email {{url}}', $response['body']['message']);
/** Get Updated Email Template */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('Please verify your email', $response['body']['subject']);
$this->assertEquals('Appwrite Custom', $response['body']['senderName']);
$this->assertEquals('custom@appwrite.io', $response['body']['senderEmail']);
$this->assertEquals('verification', $response['body']['type']);
$this->assertEquals('en-us', $response['body']['locale']);
$this->assertEquals('Please verify your email {{url}}', $response['body']['message']);
/** Get Default SMS Template */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('verification', $response['body']['type']);
$this->assertEquals('en-us', $response['body']['locale']);
$this->assertEquals('{{token}}', $response['body']['message']);
/** Update SMS template */
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'message' => 'Please verify your email {{token}}',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('verification', $response['body']['type']);
$this->assertEquals('en-us', $response['body']['locale']);
$this->assertEquals('Please verify your email {{token}}', $response['body']['message']);
/** Get Updated SMS Template */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('verification', $response['body']['type']);
$this->assertEquals('en-us', $response['body']['locale']);
$this->assertEquals('Please verify your email {{token}}', $response['body']['message']);
return $data;
}
/** @depends testGetProjectUsage */
public function testUpdateProjectAuthDuration($data): array
{
@@ -57,7 +57,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id']), true);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-User-Id'], $id);
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['name'], $name);
$dateValidator = new DatetimeValidator();
@@ -195,7 +195,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id']), true);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-User-Id'], $id);
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertNotEmpty($webhook['data']['userId']);
$dateValidator = new DatetimeValidator();
@@ -744,7 +744,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id']), true);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-User-Id'], $id);
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertNotEmpty($webhook['data']['userId']);
$this->assertNotEmpty($webhook['data']['secret']);
@@ -923,7 +923,7 @@ class WebhooksCustomClientTest extends Scope
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '', $userUid);
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertNotEmpty($webhook['data']['userId']);
$this->assertNotEmpty($webhook['data']['teamId']);