1
0
Fork 0
mirror of synced 2024-06-13 16:24:47 +12:00

Merge branch '1.4.x' into cloud-1.4.x

This commit is contained in:
Steven Nguyen 2023-08-04 18:07:33 -07:00
commit d960f85da6
No known key found for this signature in database
269 changed files with 6162 additions and 2292 deletions

1
.env
View file

@ -6,6 +6,7 @@ _APP_CONSOLE_WHITELIST_EMAILS=
_APP_CONSOLE_WHITELIST_CODES=code-zero,code-one
_APP_CONSOLE_WHITELIST_IPS=
_APP_CONSOLE_INVITES=enabled
_APP_CONSOLE_ROOT_SESSION=disabled
_APP_SYSTEM_EMAIL_NAME=Appwrite
_APP_SYSTEM_EMAIL_ADDRESS=team@appwrite.io
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=security@appwrite.io

View file

@ -1,9 +1,31 @@
# Version TBD
# Version 1.4.0
## Features
- Add error attribute to indexes and attributes [#4575](https://github.com/appwrite/appwrite/pull/4575)
- Add new index validation rules [#5710](https://github.com/appwrite/appwrite/pull/5710)
## Fixes
- Fix cascading deletes across multiple levels [DB #269](https://github.com/utopia-php/database/pull/269)
- Fix identical two-way keys not throwing duplicate exceptions [DB #273](https://github.com/utopia-php/database/pull/273)
- Fix search wildcards [DB #279](https://github.com/utopia-php/database/pull/279)
- Fix permissions returning as an object instead of list [DB #281](https://github.com/utopia-php/database/pull/281)
- Fix missing collection not found error [DB #282](https://github.com/utopia-php/database/pull/282)
## Changes
- Improve permission indexes [DB #248](https://github.com/utopia-php/database/pull/248)
- Validators back-ported to Utopia [#5439](https://github.com/appwrite/appwrite/pull/5439)
# Version 1.3.8
## Changes
- Replace Appwrite executor with OpenRuntimes Executor [#4650](https://github.com/appwrite/appwrite/pull/4650)
- Add `_APP_CONNECTIONS_MAX` env var [#4673](https://github.com/appwrite/appwrite/pull/4673)
- Increase Traefik TCP + file limits [#4673](https://github.com/appwrite/appwrite/pull/4673)
- Store build output file size [#4844](https://github.com/appwrite/appwrite/pull/4844)
# Version 1.3.8
## Bugs
- Fix audit user internal [#5809](https://github.com/appwrite/appwrite/pull/5809)
@ -25,14 +47,14 @@
## Bugs
- Fix minimum length for string attribute default values [#5606](https://github.com/appwrite/appwrite/pull/5606)
- Fix minimum length for string attribute default values [#5606](https://github.com/appwrite/appwrite/pull/5606), [#5602](https://github.com/appwrite/appwrite/pull/5602)
- Update framework to fix route mismatches [#5603](https://github.com/appwrite/appwrite/pull/5603)
# Version 1.3.4
## Bugs
- Update migration to properly migrate bucket permissiosn [#5497](https://github.com/appwrite/appwrite/pull/5497)
- Update migration to properly migrate bucket permissions [#5497](https://github.com/appwrite/appwrite/pull/5497)
# Version 1.3.3
@ -45,6 +67,7 @@
- Fixed auto-setting custom ID on nested documents [#5363](https://github.com/appwrite/appwrite/pull/5363)
- Fixed listDocuments not returning all the documents [#5395](https://github.com/appwrite/appwrite/pull/5395)
- Fixed deleting keys, webhooks, platforms and domains after deleting project [#5395](https://github.com/appwrite/appwrite/pull/5395)
- Fixed empty team prefs returning as JSON object rather array [#5361](https://github.com/appwrite/appwrite/pull/5361)
# Version 1.3.1
@ -139,6 +162,7 @@
## Changes
- Released `appwrite/console` [2.0.2](https://github.com/appwrite/console/releases/tag/2.0.2)
- Make `region` parameter optional with default for project create [#4763](https://github.com/appwrite/appwrite/pull/4763)
- Add security headers to the console endpoint [#4758](https://github.com/appwrite/appwrite/pull/4758)
## Bugs
- Fix default oauth paths [#4725](https://github.com/appwrite/appwrite/pull/4725)

View file

@ -48,6 +48,13 @@ ENV _APP_SERVER=swoole \
_APP_HOME=https://appwrite.io \
_APP_EDITION=community \
_APP_CONSOLE_WHITELIST_ROOT=enabled \
_APP_CONSOLE_WHITELIST_EMAILS= \
_APP_CONSOLE_WHITELIST_IPS= \
_APP_CONSOLE_ROOT_SESSION= \
_APP_SYSTEM_EMAIL_NAME= \
_APP_SYSTEM_EMAIL_ADDRESS= \
_APP_SYSTEM_RESPONSE_FORMAT= \
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS= \
_APP_OPTIONS_ABUSE=enabled \
_APP_OPTIONS_FORCE_HTTPS=disabled \
_APP_OPENSSL_KEY_V1=your-secret-key \

View file

@ -128,6 +128,12 @@ Choose from one of the providers below:
<br /><sub><b>Gitpod</b></sub></a>
</a>
</td>
<td align="center" width="100" height="100">
<a href="https://www.linode.com/marketplace/apps/appwrite/appwrite/">
<img width="50" height="39" src="public/images/integrations/akamai-logo.svg" alt="Akamai Logo" />
<br /><sub><b>Akamai</b></sub></a>
</a>
</td>
</tr>
</table>

View file

@ -67,6 +67,17 @@ $commonCollections = [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('labels'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('passwordHistory'),
'type' => Database::VAR_STRING,
@ -219,8 +230,19 @@ $commonCollections = [
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
]
'filters' => ['userSearch'],
],
[
'$id' => ID::custom('accessedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[
@ -285,7 +307,14 @@ $commonCollections = [
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
]
],
[
'$id' => '_key_accessedAt',
'type' => Database::INDEX_KEY,
'attributes' => ['accessedAt'],
'lengths' => [],
'orders' => [],
],
],
],
@ -1216,6 +1245,17 @@ $projectCollections = array_merge([
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('enabled'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => true,
'array' => false,
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
@ -1328,6 +1368,17 @@ $projectCollections = array_merge([
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('error'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('size'),
'type' => Database::VAR_INTEGER,
@ -1515,6 +1566,17 @@ $projectCollections = array_merge([
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('error'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('attributes'),
'type' => Database::VAR_STRING,
@ -2629,6 +2691,28 @@ $consoleCollections = array_merge([
'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,

View file

@ -237,6 +237,11 @@ return [
'description' => 'The invite does not belong to the current user.',
'code' => 401,
],
Exception::TEAM_ALREADY_EXISTS => [
'name' => Exception::TEAM_ALREADY_EXISTS,
'description' => 'Team with requested ID already exists.',
'code' => 409,
],
/** Membership */
Exception::MEMBERSHIP_NOT_FOUND => [
@ -413,9 +418,14 @@ return [
'description' => 'The document structure is invalid. Please ensure the attributes match the collection definition.',
'code' => 400,
],
Exception::DOCUMENT_MISSING_DATA => [
'name' => Exception::DOCUMENT_MISSING_DATA,
'description' => 'The document data is missing. You must provide the document data.',
'code' => 400,
],
Exception::DOCUMENT_MISSING_PAYLOAD => [
'name' => Exception::DOCUMENT_MISSING_PAYLOAD,
'description' => 'The document payload is missing.',
'description' => 'The document data and permissions are missing. You must provide either the document data or permissions to be updated.',
'code' => 400,
],
Exception::DOCUMENT_ALREADY_EXISTS => [
@ -497,6 +507,11 @@ return [
'description' => 'Index with the requested ID already exists.',
'code' => 409,
],
Exception::INDEX_INVALID => [
'name' => Exception::INDEX_INVALID,
'description' => 'Index invalid.',
'code' => 400,
],
/** Project Errors */
Exception::PROJECT_NOT_FOUND => [
@ -544,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.',

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",
],
];

View file

@ -0,0 +1,15 @@
<?php
return [
'email' => [
'verification',
'magicSession',
'recovery',
'invitation',
],
'sms' => [
'verification',
'login',
'invitation'
]
];

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>

View file

@ -0,0 +1 @@
{{token}}

View file

@ -285,7 +285,7 @@ return [
[
'key' => 'python',
'name' => 'Python',
'version' => '2.0.0',
'version' => '2.0.2',
'url' => 'https://github.com/appwrite/sdk-for-python',
'package' => 'https://pypi.org/project/appwrite/',
'enabled' => true,
@ -357,12 +357,12 @@ return [
[
'key' => 'dotnet',
'name' => '.NET',
'version' => '2.0.0',
'version' => '0.4.2',
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
'package' => 'https://www.nuget.org/packages/Appwrite',
'enabled' => false,
'enabled' => true,
'beta' => true,
'dev' => true,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_SERVER,
'prism' => 'csharp',

View file

@ -201,6 +201,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false,
],
'oidc' => [
'name' => 'OpenID Connect',
'developers' => 'https://openid.net/connect/faq/',
'icon' => 'icon-oidc',
'enabled' => true,
'sandbox' => false,
'form' => 'oidc.phtml',
'beta' => false,
'mock' => false,
],
'okta' => [
'name' => 'Okta',
'developers' => 'https://developer.okta.com/',
@ -222,7 +232,7 @@ return [ // Ordered by ABC.
'mock' => false
],
'paypalSandbox' => [
'name' => 'PayPal',
'name' => 'PayPal Sandbox',
'developers' => 'https://developer.paypal.com/docs/api/overview/',
'icon' => 'icon-paypal',
'enabled' => true,
@ -292,7 +302,7 @@ return [ // Ordered by ABC.
'mock' => false,
],
'tradeshiftBox' => [
'name' => 'Tradeshift',
'name' => 'Tradeshift Sandbox',
'developers' => 'https://developers.tradeshift.com/docs/api',
'icon' => 'icon-tradeshiftbox',
'enabled' => true,

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

View file

@ -105,6 +105,15 @@ return [
'question' => '',
'filter' => ''
],
[
'name' => '_APP_CONSOLE_ROOT_SESSION',
'description' => 'Domain policy for the Appwrite console session cookie. By default, set to \'disabled\', meaning the session cookie will be set to the domain of the Appwrite console (e.g. cloud.appwrite.io). When set to \'enabled\', the session cookie will be set to the registerable domain of the Appwrite server (e.g. appwrite.io).',
'introduction' => '',
'default' => 'disabled',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_SYSTEM_EMAIL_NAME',
'description' => 'This is the sender name value that will appear on email messages sent to developers from the Appwrite console. The default value is: \'Appwrite\'. You can use url encoded strings for spaces and special chars.',
@ -152,7 +161,7 @@ return [
],
[
'name' => '_APP_LOGGING_PROVIDER',
'description' => 'This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of \'sentry\', \'raygun\', \'appsignal\', \'logowl\'',
'description' => 'This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of \'sentry\', \'raygun\', \'appSignal\', \'logOwl\'',
'introduction' => '0.12.0',
'default' => '',
'required' => false,

View file

@ -16,9 +16,9 @@ use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Template\Template;
use Appwrite\URL\URL as URLParser;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
@ -164,10 +164,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');
@ -196,7 +197,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()),
@ -218,8 +219,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);
}
@ -258,12 +261,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();
@ -272,7 +276,7 @@ App::post('/v1/account/sessions/email')
Query::equal('email', [$email]),
]);
if (!$profile || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) {
if (!$profile || empty($profile->getAttribute('passwordUpdate')) || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) {
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
@ -280,6 +284,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'));
@ -289,8 +295,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
@ -303,35 +309,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)
;
@ -344,7 +350,7 @@ App::post('/v1/account/sessions/email')
;
$events
->setParam('userId', $profile->getId())
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
;
@ -419,7 +425,7 @@ App::get('/v1/account/sessions/oauth2/:provider')
App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->desc('OAuth2 Callback')
->groups(['api', 'account'])
->groups(['account'])
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('scope', 'public')
->label('docs', false)
@ -443,7 +449,7 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->desc('OAuth2 Callback')
->groups(['api', 'account'])
->groups(['account'])
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('scope', 'public')
->label('origin', '*')
@ -567,10 +573,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);
@ -585,9 +596,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;
@ -605,7 +619,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()),
@ -628,7 +642,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);
}
@ -644,7 +659,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator();
$expire = DateTime::addSeconds(new \DateTime(), $duration);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge([
'$id' => ID::unique(),
@ -661,13 +676,12 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$isAnonymousUser = Auth::isAnonymousUser($user);
if (empty($user->getAttribute('email'))) {
$user->setAttribute('email', $oauth2->getUserEmail($accessToken));
}
if ($isAnonymousUser) {
$user
->setAttribute('name', $oauth2->getUserName($accessToken))
->setAttribute('email', $oauth2->getUserEmail($accessToken))
;
if (empty($user->getAttribute('name'))) {
$user->setAttribute('name', $oauth2->getUserName($accessToken));
}
$user
@ -741,12 +755,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');
@ -756,9 +771,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) {
@ -771,7 +787,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()),
@ -792,11 +808,13 @@ 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();
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM));
$token = new Document([
'$id' => ID::unique(),
@ -829,10 +847,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"))
@ -895,32 +920,35 @@ 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());
$secret = Auth::tokenGenerator();
$expire = DateTime::addSeconds(new \DateTime(), $duration);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
[
@ -960,9 +988,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');
}
@ -1015,11 +1043,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');
@ -1029,9 +1059,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) {
@ -1043,8 +1074,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()),
@ -1065,11 +1095,13 @@ App::post('/v1/account/sessions/phone')
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $phone])
])));
]);
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
}
$secret = Auth::codeGenerator();
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_PHONE);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_PHONE));
$token = new Document([
'$id' => ID::unique(),
@ -1093,9 +1125,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(
@ -1132,30 +1174,33 @@ 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());
$secret = Auth::tokenGenerator();
$expire = DateTime::addSeconds(new \DateTime(), $duration);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
[
@ -1193,7 +1238,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');
@ -1276,7 +1321,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()),
@ -1297,15 +1342,16 @@ 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;
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator();
$expire = DateTime::addSeconds(new \DateTime(), $duration);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
[
@ -1475,6 +1521,7 @@ App::get('/v1/account/sessions')
$session->setAttribute('countryName', $countryName);
$session->setAttribute('current', ($current == $session->getId()) ? true : false);
$session->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration)));
$sessions[$key] = $session;
}
@ -1496,7 +1543,7 @@ App::get('/v1/account/logs')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('user')
->inject('locale')
@ -1579,7 +1626,7 @@ App::get('/v1/account/sessions/:sessionId')
$session
->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret)))
->setAttribute('countryName', $countryName)
->setAttribute('expire', DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration))
->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration)))
;
return $response->dynamic($session, Response::MODEL_SESSION);
@ -1613,9 +1660,7 @@ App::patch('/v1/account/name')
->inject('events')
->action(function (string $name, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$user
->setAttribute('name', $name)
->setAttribute('search', implode(' ', [$user->getId(), $name, $user->getAttribute('email', ''), $user->getAttribute('phone', '')]));
$user->setAttribute('name', $name);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
@ -1708,10 +1753,11 @@ App::patch('/v1/account/email')
->inject('dbForProject')
->inject('events')
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
// passwordUpdate will be empty if the user has never set a password
$passwordUpdate = $user->getAttribute('passwordUpdate');
if (
!$isAnonymousUser &&
!empty($passwordUpdate) &&
!Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))
) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
@ -1720,16 +1766,21 @@ App::patch('/v1/account/email')
$email = \strtolower($email);
$user
->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS) : $user->getAttribute('password', ''))
->setAttribute('hash', $isAnonymousUser ? Auth::DEFAULT_ALGO : $user->getAttribute('hash', ''))
->setAttribute('hashOptions', $isAnonymousUser ? Auth::DEFAULT_ALGO_OPTIONS : $user->getAttribute('hashOptions', ''))
->setAttribute('email', $email)
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $email, $user->getAttribute('phone', '')]));
;
if (empty($passwordUpdate)) {
$user
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('passwordUpdate', DateTime::now());
}
try {
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
} catch (Duplicate $th) {
} catch (Duplicate) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@ -1762,11 +1813,11 @@ App::patch('/v1/account/phone')
->inject('dbForProject')
->inject('events')
->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
// passwordUpdate will be empty if the user has never set a password
$passwordUpdate = $user->getAttribute('passwordUpdate');
if (
!$isAnonymousUser &&
!empty($passwordUpdate) &&
!Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))
) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
@ -1775,7 +1826,15 @@ App::patch('/v1/account/phone')
$user
->setAttribute('phone', $phone)
->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $user->getAttribute('email', ''), $phone]));
;
if (empty($passwordUpdate)) {
$user
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('passwordUpdate', DateTime::now());
}
try {
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
@ -2009,7 +2068,7 @@ App::patch('/v1/account/sessions/:sessionId')
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$session->setAttribute('expire', DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration));
$session->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration)));
$events
->setParam('userId', $user->getId())
@ -2105,12 +2164,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');
@ -2130,6 +2190,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);
}
@ -2168,6 +2230,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"))
@ -2235,9 +2305,10 @@ 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('events')
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Database $dbForProject, 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);
}
@ -2264,6 +2335,8 @@ App::put('/v1/account/recovery')
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('emailVerification', true));
$user->setAttributes($profile->getArrayCopy());
$recoveryDocument = $dbForProject->getDocument('tokens', $recovery);
/**
@ -2348,6 +2421,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"))
@ -2370,7 +2452,7 @@ App::post('/v1/account/verification')
->setBody($body)
->setFrom($from)
->setRecipient($user->getAttribute('email'))
->setName($user->getAttribute('name'))
->setName($user->getAttribute('name') ?? '')
->trigger()
;
@ -2432,6 +2514,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);
/**
@ -2442,7 +2526,7 @@ App::put('/v1/account/verification')
$dbForProject->deleteCachedDocument('users', $profile->getId());
$events
->setParam('userId', $user->getId())
->setParam('userId', $userId)
->setParam('tokenId', $verificationDocument->getId())
;
@ -2471,7 +2555,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);
@ -2484,7 +2570,6 @@ App::post('/v1/account/verification/phone')
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$verificationSecret = Auth::tokenGenerator();
$secret = Auth::codeGenerator();
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM);
@ -2510,9 +2595,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()
;
@ -2520,13 +2615,13 @@ App::post('/v1/account/verification/phone')
->setParam('userId', $user->getId())
->setParam('tokenId', $verification->getId())
->setPayload($response->output(
$verification->setAttribute('secret', $verificationSecret),
$verification->setAttribute('secret', $secret),
Response::MODEL_TOKEN
))
;
// Hide secret for clients
$verification->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $verificationSecret : '');
$verification->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $secret : '');
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -2573,6 +2668,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);
/**

View file

@ -1,54 +1,57 @@
<?php
use Utopia\App;
use Appwrite\Auth\Auth;
use Appwrite\Detector\Detector;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Collections;
use Appwrite\Utopia\Database\Validator\Queries\Databases;
use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Helpers\ID;
use Utopia\Validator\Boolean;
use Utopia\Validator\FloatValidator;
use Utopia\Validator\Integer;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
use Utopia\Validator\ArrayList;
use Utopia\Validator\JSON;
use Utopia\Config\Config;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\Structure;
use Utopia\Database\Validator\UID;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\Restricted as RestrictedException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Index as IndexValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Queries\Document as DocumentQueriesValidator;
use Utopia\Database\Validator\Queries\Documents;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\Structure;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
use Appwrite\Auth\Auth;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\FloatValidator;
use Utopia\Validator\IP;
use Utopia\Validator\URL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Response;
use Appwrite\Detector\Detector;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Collections;
use Appwrite\Utopia\Database\Validator\Queries\Databases;
use Appwrite\Utopia\Database\Validator\Queries\Document as DocumentValidator;
use Appwrite\Utopia\Database\Validator\Queries\Documents;
use Utopia\Config\Config;
use MaxMind\Db\Reader;
use Utopia\Validator\Integer;
use Utopia\Validator\JSON;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
/**
* Create attribute of varying type
@ -325,22 +328,22 @@ function updateAttribute(
}
if ($type === Database::VAR_RELATIONSHIP) {
$options = \array_merge($attribute->getAttribute('options', []), $options);
$attribute->setAttribute('options', $options);
$primaryDocumentOptions = \array_merge($attribute->getAttribute('options', []), $options);
$attribute->setAttribute('options', $primaryDocumentOptions);
$dbForProject->updateRelationship(
collection: $collectionId,
id: $key,
onDelete: $options['onDelete'],
onDelete: $primaryDocumentOptions['onDelete'],
);
if ($options['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $db->getInternalId(), $options['relatedCollection']);
$relatedAttribute = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
if ($primaryDocumentOptions['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $db->getInternalId(), $primaryDocumentOptions['relatedCollection']);
$relatedAttribute = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $primaryDocumentOptions['twoWayKey']);
$relatedOptions = \array_merge($relatedAttribute->getAttribute('options'), $options);
$relatedAttribute->setAttribute('options', $relatedOptions);
$dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey'], $relatedAttribute);
$dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $primaryDocumentOptions['twoWayKey'], $relatedAttribute);
$dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId());
}
} else {
@ -381,11 +384,12 @@ App::post('/v1/databases')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DATABASE) // Model for database needs to be created
->param('databaseId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
->param('name', '', new Text(128), 'Database name. Max length: 128 chars.')
->param('enabled', true, new Boolean(), 'Is database enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('events')
->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, Event $events) {
->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $events) {
$databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId;
@ -393,6 +397,7 @@ App::post('/v1/databases')
$dbForProject->createDocument('databases', new Document([
'$id' => $databaseId,
'name' => $name,
'enabled' => $enabled,
'search' => implode(' ', [$databaseId, $name]),
]));
$database = $dbForProject->getDocument('databases', $databaseId);
@ -429,7 +434,7 @@ App::post('/v1/databases')
]);
}
$dbForProject->createCollection('database_' . $database->getInternalId(), $attributes, $indexes);
} catch (DuplicateException $th) {
} catch (DuplicateException) {
throw new Exception(Exception::DATABASE_ALREADY_EXISTS);
}
@ -464,10 +469,9 @@ App::get('/v1/databases')
}
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$databaseId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('databases', $databaseId);
@ -502,7 +506,7 @@ App::get('/v1/databases/:databaseId')
->inject('dbForProject')
->action(function (string $databaseId, Response $response, Database $dbForProject) {
$database = $dbForProject->getDocument('databases', $databaseId);
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
@ -523,7 +527,7 @@ App::get('/v1/databases/:databaseId/logs')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('databaseId', '', new UID(), 'Database ID.')
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
@ -613,12 +617,13 @@ App::put('/v1/databases/:databaseId')
->label('sdk.response.model', Response::MODEL_DATABASE)
->param('databaseId', '', new UID(), 'Database ID.')
->param('name', null, new Text(128), 'Database name. Max length: 128 chars.')
->param('enabled', true, new Boolean(), 'Is database enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('events')
->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, Event $events) {
->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $events) {
$database = $dbForProject->getDocument('databases', $databaseId);
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
@ -627,8 +632,9 @@ App::put('/v1/databases/:databaseId')
try {
$database = $dbForProject->updateDocument('databases', $databaseId, $database
->setAttribute('name', $name)
->setAttribute('enabled', $enabled)
->setAttribute('search', implode(' ', [$databaseId, $name])));
} catch (AuthorizationException $exception) {
} catch (AuthorizationException) {
throw new Exception(Exception::USER_UNAUTHORIZED);
} catch (StructureException $exception) {
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, 'Bad structure. ' . $exception->getMessage());
@ -705,14 +711,16 @@ App::post('/v1/databases/:databaseId/collections')
->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](/docs/permissions).', true)
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is collection enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('mode')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, Response $response, Database $dbForProject, Event $events) {
->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $events) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
@ -728,7 +736,7 @@ App::post('/v1/databases/:databaseId/collections')
'databaseId' => $databaseId,
'$permissions' => $permissions ?? [],
'documentSecurity' => $documentSecurity,
'enabled' => true,
'enabled' => $enabled,
'name' => $name,
'search' => implode(' ', [$collectionId, $name]),
]));
@ -768,11 +776,12 @@ App::get('/v1/databases/:databaseId/collections')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject) {
->inject('mode')
->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
@ -783,7 +792,7 @@ App::get('/v1/databases/:databaseId/collections')
}
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
@ -821,11 +830,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId')
->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response')
->inject('dbForProject')
->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject) {
->inject('mode')
->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, string $mode) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
@ -852,14 +862,14 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs')
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
@ -955,12 +965,13 @@ App::put('/v1/databases/:databaseId/collections/:collectionId')
->param('enabled', true, new Boolean(), 'Is collection enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('mode')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, Event $events) {
->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $events) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
@ -1017,13 +1028,14 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId')
->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response')
->inject('dbForProject')
->inject('mode')
->inject('events')
->inject('deletes')
->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, Event $events, Delete $deletes) {
->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, string $mode, Event $events, Delete $deletes) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
@ -1041,15 +1053,13 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId')
$deletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($collection)
;
->setDocument($collection);
$events
->setContext('database', $database)
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collection->getId())
->setPayload($response->output($collection, Response::MODEL_COLLECTION))
;
->setPayload($response->output($collection, Response::MODEL_COLLECTION));
$response->noContent();
});
@ -1554,7 +1564,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati
Database $dbForProject,
EventDatabase $database,
Event $events
) {
) {
$key ??= $relatedCollectionId;
$twoWayKey ??= $collectionId;
@ -1612,7 +1622,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes')
->inject('dbForProject')
->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
@ -1661,7 +1671,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
->inject('dbForProject')
->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
@ -2114,7 +2124,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/
$events,
type: Database::VAR_RELATIONSHIP,
required: false,
options : [
options: [
'onDelete' => $onDelete
]
);
@ -2153,7 +2163,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events) {
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$db = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
@ -2206,8 +2216,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
->setType(DATABASE_TYPE_DELETE_ATTRIBUTE)
->setCollection($collection)
->setDatabase($db)
->setDocument($attribute)
;
->setDocument($attribute);
// Select response model based on type and format
$type = $attribute->getAttribute('type');
@ -2235,8 +2244,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
->setParam('attributeId', $attribute->getId())
->setContext('collection', $collection)
->setContext('database', $db)
->setPayload($response->output($attribute, $model))
;
->setPayload($response->output($attribute, $model));
$response->noContent();
});
@ -2268,11 +2276,12 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $database, Event $events) {
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$db = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId);
if ($collection->isEmpty()) {
@ -2291,7 +2300,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
}
// Convert Document[] to array of attribute metadata
$oldAttributes = \array_map(fn ($a) => $a->getArrayCopy(), $collection->getAttribute('attributes'));
$oldAttributes = \array_map(fn($a) => $a->getArrayCopy(), $collection->getAttribute('attributes'));
$oldAttributes[] = [
'key' => '$id',
@ -2353,21 +2362,28 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$lengths[$i] = ($attributeType === Database::VAR_STRING) ? $attributeSize : null;
}
$index = new Document([
'$id' => ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key),
'key' => $key,
'status' => 'processing', // processing, available, failed, deleting, stuck
'databaseInternalId' => $db->getInternalId(),
'databaseId' => $databaseId,
'collectionInternalId' => $collection->getInternalId(),
'collectionId' => $collectionId,
'type' => $type,
'attributes' => $attributes,
'lengths' => $lengths,
'orders' => $orders,
]);
$validator = new IndexValidator($dbForProject->getAdapter()->getMaxIndexLength());
if (!$validator->isValid($collection->setAttribute('indexes', $index, Document::SET_TYPE_APPEND))) {
throw new Exception(Exception::INDEX_INVALID, $validator->getDescription());
}
try {
$index = $dbForProject->createDocument('indexes', new Document([
'$id' => ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key),
'key' => $key,
'status' => 'processing', // processing, available, failed, deleting, stuck
'databaseInternalId' => $db->getInternalId(),
'databaseId' => $databaseId,
'collectionInternalId' => $collection->getInternalId(),
'collectionId' => $collectionId,
'type' => $type,
'attributes' => $attributes,
'lengths' => $lengths,
'orders' => $orders,
]));
} catch (DuplicateException $th) {
$index = $dbForProject->createDocument('indexes', $index);
} catch (DuplicateException) {
throw new Exception(Exception::INDEX_ALREADY_EXISTS);
}
@ -2377,16 +2393,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
->setType(DATABASE_TYPE_CREATE_INDEX)
->setDatabase($db)
->setCollection($collection)
->setDocument($index)
;
->setDocument($index);
$events
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collection->getId())
->setParam('indexId', $index->getId())
->setContext('collection', $collection)
->setContext('database', $db)
;
->setContext('database', $db);
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@ -2411,7 +2425,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes')
->inject('dbForProject')
->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
@ -2449,7 +2463,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->inject('dbForProject')
->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
@ -2460,21 +2474,15 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$indexes = $collection->getAttribute('indexes');
// Search for index
$indexIndex = array_search($key, array_map(fn($idx) => $idx['key'], $indexes));
if ($indexIndex === false) {
$index = $collection->find('key', $key, 'indexes');
if (empty($index)) {
throw new Exception(Exception::INDEX_NOT_FOUND);
}
$index = $indexes[$indexIndex];
$index->setAttribute('collectionId', $database->getInternalId() . '_' . $collectionId);
$response->dynamic($index, Response::MODEL_INDEX);
});
App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default'])
->desc('Delete Index')
@ -2498,7 +2506,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events) {
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$db = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
@ -2526,8 +2534,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->setType(DATABASE_TYPE_DELETE_INDEX)
->setDatabase($db)
->setCollection($collection)
->setDocument($index)
;
->setDocument($index);
$events
->setParam('databaseId', $databaseId)
@ -2535,8 +2542,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->setParam('indexId', $index->getId())
->setContext('collection', $collection)
->setContext('database', $db)
->setPayload($response->output($index, Response::MODEL_INDEX))
;
->setPayload($response->output($index, Response::MODEL_INDEX));
$response->noContent();
});
@ -2548,7 +2554,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].create')
->label('scope', 'documents.write')
->label('audits.event', 'document.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
@ -2576,7 +2582,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
if (empty($data)) {
throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD);
throw new Exception(Exception::DOCUMENT_MISSING_DATA);
}
if (isset($data['$id'])) {
@ -2585,7 +2591,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
@ -2660,7 +2666,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
@ -2739,7 +2745,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
@ -2772,8 +2778,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
->setParam('collectionId', $collection->getId())
->setParam('documentId', $document->getId())
->setContext('collection', $collection)
->setContext('database', $database)
;
->setContext('database', $database);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -2801,9 +2806,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
->inject('mode')
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
@ -2832,7 +2837,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$queries = Query::parseQueries($queries);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
$documentId = $cursor->getValue();
@ -2863,7 +2868,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
@ -2879,8 +2884,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
}
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
$relatedCollection = Authorization::skip(fn() =>
$dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId));
$relatedCollection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId));
foreach ($relations as $index => $doc) {
if ($doc instanceof Document) {
@ -2900,7 +2904,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
return true;
};
// The linter is forcing this indentation
// The linter is forcing this indentation
foreach ($documents as $document) {
$processDocument($collection, $document);
}
@ -2934,9 +2938,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
@ -2949,7 +2953,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
}
// Validate queries
$queriesValidator = new DocumentValidator($collection->getAttribute('attributes'));
$queriesValidator = new DocumentQueriesValidator($collection->getAttribute('attributes'));
$validQueries = $queriesValidator->isValid($queries);
if (!$validQueries) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $queriesValidator->getDescription());
@ -2974,7 +2978,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
@ -3020,14 +3024,14 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
@ -3142,9 +3146,9 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD);
}
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
@ -3221,7 +3225,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
@ -3289,7 +3293,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
try {
$document = $dbForProject->withRequestTimestamp(
$requestTimestamp,
fn () => $dbForProject->updateDocument(
fn() => $dbForProject->updateDocument(
'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(),
$document->getId(),
$newDocument
@ -3310,7 +3314,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
@ -3343,8 +3347,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
->setParam('collectionId', $collection->getId())
->setParam('documentId', $document->getId())
->setContext('collection', $collection)
->setContext('database', $database)
;
->setContext('database', $database);
$response->dynamic($document, Response::MODEL_DOCUMENT);
});
@ -3379,9 +3382,9 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $events, Delete $deletes, string $mode) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
if ($database->isEmpty() || (!$database->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
@ -3416,7 +3419,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
@ -3447,7 +3450,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
$checkPermissions($collection, $document);
Authorization::skip(fn () => $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $database, $collection, $documentId) {
Authorization::skip(fn() => $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $database, $collection, $documentId) {
try {
$dbForProject->deleteDocument(
'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(),
@ -3470,7 +3473,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
@ -3500,8 +3503,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
$deletes
->setType(DELETE_TYPE_AUDIT)
->setDocument($document)
;
->setDocument($document);
$events
->setParam('databaseId', $databaseId)
@ -3509,8 +3511,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
->setParam('documentId', $document->getId())
->setContext('collection', $collection)
->setContext('database', $database)
->setPayload($response->output($document, Response::MODEL_DOCUMENT))
;
->setPayload($response->output($document, Response::MODEL_DOCUMENT));
$response->noContent();
});

View file

@ -138,7 +138,7 @@ App::get('/v1/functions')
}
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
@ -611,11 +611,13 @@ App::post('/v1/functions/:functionId/deployments')
$end = $request->getContentRangeEnd();
$fileSize = $request->getContentRangeSize();
$deploymentId = $request->getHeader('x-appwrite-id', $deploymentId);
if (is_null($start) || is_null($end) || is_null($fileSize)) {
// TODO make `end >= $fileSize` in next breaking version
if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) {
throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE);
}
if ($end === $fileSize) {
// TODO remove the condition that checks `$end === $fileSize` in next breaking version
if ($end === $fileSize - 1 || $end === $fileSize) {
//if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk
$chunks = $chunk = -1;
} else {
@ -776,7 +778,7 @@ App::get('/v1/functions/:functionId/deployments')
$queries[] = Query::equal('resourceType', ['functions']);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
@ -1209,7 +1211,7 @@ App::get('/v1/functions/:functionId/executions')
$queries[] = Query::equal('functionId', [$function->getId()]);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */

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'])

View file

@ -9,7 +9,7 @@ use Appwrite\Network\Validator\CNAME;
use Utopia\Validator\Domain as DomainValidator;
use Appwrite\Network\Validator\Origin;
use Utopia\Validator\URL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\ProjectId;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
@ -27,15 +27,20 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Utopia\Cache\Cache;
use Utopia\Pools\Group;
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'])
@ -56,7 +61,7 @@ App::post('/v1/projects')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.')
->param('name', null, new Text(128), 'Project name. Max length: 128 chars.')
->param('teamId', '', new UID(), 'Team unique ID.')
->param('region', App::getEnv('_APP_REGION', 'default'), new Whitelist(array_keys(array_filter(Config::getParam('regions'), fn($config) => !$config['disabled']))), 'Project Region.', true)
@ -240,7 +245,7 @@ App::get('/v1/projects')
}
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
@ -333,6 +338,46 @@ App::patch('/v1/projects/:projectId')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/team')
->desc('Update Project Team')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateTeam')
->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('teamId', '', new UID(), 'Team ID of the team to transfer project to.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $teamId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
$team = $dbForConsole->getDocument('teams', $teamId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
if ($team->isEmpty()) {
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('teamId', $teamId)
->setAttribute('$permissions', [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
]));
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/service')
->desc('Update service status')
->groups(['api', 'projects'])
@ -364,6 +409,40 @@ App::patch('/v1/projects/:projectId/service')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/service/all')
->desc('Update all service status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateServiceStatusAll')
->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('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$allServices = array_keys(array_filter(Config::getParam('services'), fn($element) => $element['optional']));
$services = [];
foreach ($allServices as $service) {
$services[$service] = $status;
}
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/oauth2')
->desc('Update Project OAuth2')
->groups(['api', 'projects'])
@ -607,17 +686,11 @@ App::delete('/v1/projects/:projectId')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('password', '', new Password(), 'Your user password for confirmation. Must be at least 8 chars.')
->inject('response')
->inject('user')
->inject('dbForConsole')
->inject('deletes')
->action(function (string $projectId, string $password, Response $response, Document $user, Database $dbForConsole, Delete $deletes) {
if (!Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
->action(function (string $projectId, Response $response, Document $user, Database $dbForConsole, Delete $deletes) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@ -749,7 +822,7 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId')
}
$webhook = $dbForConsole->findOne('webhooks', [
Query::equal('_uid', [$webhookId]),
Query::equal('$id', [$webhookId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -791,7 +864,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
$security = ($security === '1' || $security === 'true' || $security === 1 || $security === true);
$webhook = $dbForConsole->findOne('webhooks', [
Query::equal('_uid', [$webhookId]),
Query::equal('$id', [$webhookId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -837,7 +910,7 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
}
$webhook = $dbForConsole->findOne('webhooks', [
Query::equal('_uid', [$webhookId]),
Query::equal('$id', [$webhookId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -875,7 +948,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
}
$webhook = $dbForConsole->findOne('webhooks', [
Query::equal('_uid', [$webhookId]),
Query::equal('$id', [$webhookId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -997,7 +1070,7 @@ App::get('/v1/projects/:projectId/keys/:keyId')
}
$key = $dbForConsole->findOne('keys', [
Query::equal('_uid', [$keyId]),
Query::equal('$id', [$keyId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -1034,7 +1107,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
}
$key = $dbForConsole->findOne('keys', [
Query::equal('_uid', [$keyId]),
Query::equal('$id', [$keyId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -1077,7 +1150,7 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
}
$key = $dbForConsole->findOne('keys', [
Query::equal('_uid', [$keyId]),
Query::equal('$id', [$keyId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -1199,7 +1272,7 @@ App::get('/v1/projects/:projectId/platforms/:platformId')
}
$platform = $dbForConsole->findOne('platforms', [
Query::equal('_uid', [$platformId]),
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -1236,7 +1309,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
}
$platform = $dbForConsole->findOne('platforms', [
Query::equal('_uid', [$platformId]),
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -1280,7 +1353,7 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
}
$platform = $dbForConsole->findOne('platforms', [
Query::equal('_uid', [$platformId]),
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -1420,7 +1493,7 @@ App::get('/v1/projects/:projectId/domains/:domainId')
}
$domain = $dbForConsole->findOne('domains', [
Query::equal('_uid', [$domainId]),
Query::equal('$id', [$domainId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -1454,7 +1527,7 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification')
}
$domain = $dbForConsole->findOne('domains', [
Query::equal('_uid', [$domainId]),
Query::equal('$id', [$domainId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -1514,7 +1587,7 @@ App::delete('/v1/projects/:projectId/domains/:domainId')
}
$domain = $dbForConsole->findOne('domains', [
Query::equal('_uid', [$domainId]),
Query::equal('$id', [$domainId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
@ -1536,3 +1609,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);
});

View file

@ -166,7 +166,7 @@ App::get('/v1/storage/buckets')
}
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
@ -438,11 +438,13 @@ App::post('/v1/storage/buckets/:bucketId/files')
$end = $request->getContentRangeEnd();
$fileSize = $request->getContentRangeSize();
$fileId = $request->getHeader('x-appwrite-id', $fileId);
if (is_null($start) || is_null($end) || is_null($fileSize)) {
// TODO make `end >= $fileSize` in next breaking version
if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) {
throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE);
}
if ($end === $fileSize) {
// TODO remove the condition that checks `$end === $fileSize` in next breaking version
if ($end === $fileSize - 1 || $end === $fileSize) {
//if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to -1 notify it's last chunk
$chunks = $chunk = -1;
} else {
@ -508,6 +510,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
}
$mimeType = $deviceFiles->getFileMimeType($path); // Get mime-type before compression and encryption
$fileHash = $deviceFiles->getFileHash($path); // Get file hash before compression and encryption
$data = '';
// Compression
$algorithm = $bucket->getAttribute('compression', COMPRESSION_TYPE_NONE);
@ -541,7 +544,6 @@ App::post('/v1/storage/buckets/:bucketId/files')
}
$sizeActual = $deviceFiles->getFileSize($path);
$fileHash = $deviceFiles->getFileHash($path);
$openSSLVersion = null;
$openSSLCipher = null;
@ -696,7 +698,7 @@ App::get('/v1/storage/buckets/:bucketId/files')
}
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
@ -1247,13 +1249,14 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->label('sdk.response.model', Response::MODEL_FILE)
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
->param('name', null, new Text(255), 'Name of the file', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permission string. By default, the current permissions are inherited. [Learn more about permissions](/docs/permissions).', true)
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('mode')
->inject('events')
->action(function (string $bucketId, string $fileId, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $events) {
->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $events) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -1309,6 +1312,10 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
$file->setAttribute('$permissions', $permissions);
if (!is_null($name)) {
$file->setAttribute('name', $name);
}
if ($fileSecurity && !$valid) {
try {
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);

View file

@ -12,11 +12,11 @@ use Appwrite\Network\Validator\Email;
use Utopia\Validator\Host;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Memberships;
use Appwrite\Utopia\Database\Validator\Queries\Teams;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
@ -67,18 +67,23 @@ App::post('/v1/teams')
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
$team = Authorization::skip(fn() => $dbForProject->createDocument('teams', new Document([
'$id' => $teamId,
'$permissions' => [
Permission::read(Role::team($teamId)),
Permission::update(Role::team($teamId, 'owner')),
Permission::delete(Role::team($teamId, 'owner')),
],
'name' => $name,
'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1,
'prefs' => new \stdClass(),
'search' => implode(' ', [$teamId, $name]),
])));
try {
$team = Authorization::skip(fn() => $dbForProject->createDocument('teams', new Document([
'$id' => $teamId,
'$permissions' => [
Permission::read(Role::team($teamId)),
Permission::update(Role::team($teamId, 'owner')),
Permission::delete(Role::team($teamId, 'owner')),
],
'name' => $name,
'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1,
'prefs' => new \stdClass(),
'search' => implode(' ', [$teamId, $name]),
])));
} catch (Duplicate $th) {
throw new Exception(Exception::TEAM_ALREADY_EXISTS);
}
if (!$isPrivilegedUser && !$isAppUser) { // Don't add user on server mode
if (!\in_array('owner', $roles)) {
@ -148,7 +153,7 @@ App::get('/v1/teams')
}
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
@ -530,6 +535,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'));
@ -559,9 +573,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();
}
}
@ -617,7 +641,7 @@ App::get('/v1/teams/:teamId/memberships')
$queries[] = Query::equal('teamId', [$teamId]);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
@ -831,7 +855,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()) {
@ -847,7 +871,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
@ -990,7 +1014,7 @@ App::get('/v1/teams/:teamId/logs')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('teamId', '', new UID(), 'Team ID.')
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')

View file

@ -8,10 +8,10 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Users;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Audit\Audit;
@ -28,6 +28,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
@ -65,6 +66,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'phone' => $phone,
'phoneVerification' => false,
'status' => true,
'labels' => [],
'password' => $password,
'passwordHistory' => is_null($password) && $passwordHistory === 0 ? [] : [$password],
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
@ -377,7 +379,7 @@ App::get('/v1/users')
}
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
@ -543,7 +545,7 @@ App::get('/v1/users/:userId/logs')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('userId', '', new UID(), 'User ID.')
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
@ -648,6 +650,45 @@ App::patch('/v1/users/:userId/status')
$response->dynamic($user, Response::MODEL_USER);
});
App::put('/v1/users/:userId/labels')
->desc('Update User Labels')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.labels')
->label('scope', 'users.write')
->label('audits.event', 'user.update')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateLabels')
->label('sdk.description', '/docs/references/users/update-user-labels.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new UID(), 'User ID.')
->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), 5), 'Array of user labels. Replaces the previous labels. Maximum of 5 labels are allowed, each up to 36 alphanumeric characters long.')
->inject('response')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, array $labels, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$user->setAttribute('labels', (array) \array_values(\array_unique($labels)));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$events
->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/verification')
->desc('Update Email Verification')
->groups(['api', 'users'])
@ -746,10 +787,7 @@ App::patch('/v1/users/:userId/name')
throw new Exception(Exception::USER_NOT_FOUND);
}
$user
->setAttribute('name', $name)
->setAttribute('search', \implode(' ', [$user->getId(), $user->getAttribute('email', ''), $name, $user->getAttribute('phone', '')]));
;
$user->setAttribute('name', $name);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
@ -849,7 +887,8 @@ App::patch('/v1/users/:userId/email')
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
->setAttribute('search', \implode(' ', [$user->getId(), $email, $user->getAttribute('name', ''), $user->getAttribute('phone', '')]));
;
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
@ -892,7 +931,6 @@ App::patch('/v1/users/:userId/phone')
$user
->setAttribute('phone', $number)
->setAttribute('phoneVerification', false)
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $user->getAttribute('email', ''), $number]));
;
try {

View file

@ -41,6 +41,7 @@ Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
App::init()
->groups(['api'])
->inject('utopia')
->inject('request')
->inject('response')
@ -49,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
*/
@ -135,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);
}
@ -173,13 +175,21 @@ App::init()
$endDomain->getRegisterable() !== ''
);
Config::setParam('cookieDomain', (
$request->getHostname() === 'localhost' ||
$request->getHostname() === 'localhost:' . $request->getPort() ||
(\filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false)
)
? null
: '.' . $request->getHostname());
$isLocalHost = $request->getHostname() === 'localhost' || $request->getHostname() === 'localhost:' . $request->getPort();
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
$isConsoleRootSession = App::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
Config::setParam(
'cookieDomain',
$isLocalHost || $isIpAddress
? null
: ($isConsoleProject && $isConsoleRootSession
? '.' . $selfDomain->getRegisterable()
: '.' . $request->getHostname()
)
);
/*
* Response format

View file

@ -285,7 +285,7 @@ App::post('/v1/mock/tests/general/upload')
$id = $request->getHeader('x-appwrite-id', '');
$file['size'] = (\is_array($file['size'])) ? $file['size'][0] : $file['size'];
if (is_null($start) || is_null($end) || is_null($size)) {
if (is_null($start) || is_null($end) || is_null($size) || $end >= $size) {
throw new Exception(Exception::GENERAL_MOCK, 'Invalid content-range header');
}
@ -301,11 +301,11 @@ App::post('/v1/mock/tests/general/upload')
throw new Exception(Exception::GENERAL_MOCK, 'All chunked request must have id header (except first)');
}
if ($end !== $size && $end - $start + 1 !== $chunkSize) {
if ($end !== $size - 1 && $end - $start + 1 !== $chunkSize) {
throw new Exception(Exception::GENERAL_MOCK, 'Chunk size must be 5MB (except last chunk)');
}
if ($end !== $size && $file['size'] !== $chunkSize) {
if ($end !== $size - 1 && $file['size'] !== $chunkSize) {
throw new Exception(Exception::GENERAL_MOCK, 'Wrong chunk size');
}
@ -313,11 +313,11 @@ App::post('/v1/mock/tests/general/upload')
throw new Exception(Exception::GENERAL_MOCK, 'Chunk size must be 5MB or less');
}
if ($end !== $size) {
if ($end !== $size - 1) {
$response->json([
'$id' => ID::custom('newfileid'),
'chunksTotal' => $file['size'] / $chunkSize,
'chunksUploaded' => $start / $chunkSize
'chunksTotal' => (int) ceil($size / ($end + 1 - $start)),
'chunksUploaded' => ceil($start / $chunkSize) + 1
]);
}
} else {

View file

@ -6,6 +6,7 @@ use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Extend\Exception;
use Appwrite\Event\Usage;
use Appwrite\Messaging\Adapter\Realtime;
@ -158,7 +159,8 @@ App::init()
->inject('dbForProject')
->inject('queueForUsage')
->inject('mode')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Delete $deletes, EventDatabase $database, Database $dbForProject, Usage $queueForUsage, string $mode) use ($databaseListener) {
->inject('mails')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Delete $deletes, EventDatabase $database, Database $dbForProject, Usage $queueForUsage, string $mode, Mail $mails) use ($databaseListener) {
$route = $utopia->match($request);
@ -240,6 +242,17 @@ App::init()
->setProject($project)
->setUser($user);
$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);
@ -406,6 +419,7 @@ App::shutdown()
->inject('request')
->inject('response')
->inject('project')
->inject('user')
->inject('events')
->inject('audits')
->inject('deletes')
@ -414,7 +428,8 @@ App::shutdown()
->inject('queueForFunctions')
->inject('queueForUsage')
->inject('mode')
->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Delete $deletes, EventDatabase $database, Database $dbForProject, Func $queueForFunctions, Usage $queueForUsage, string $mode) use ($parseLabel) {
->inject('dbForConsole')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Delete $deletes, EventDatabase $database, Database $dbForProject, Func $queueForFunctions, Usage $queueForUsage, string $mode, Database $dbForConsole) use ($parseLabel) {
$responsePayload = $response->getPayload();
@ -475,7 +490,6 @@ App::shutdown()
$route = $utopia->match($request);
$requestParams = $route->getParamsValues();
$user = $audits->getUser();
/**
* Audit labels
@ -488,10 +502,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);
}
@ -591,6 +602,22 @@ App::shutdown()
->setProject($project)
->trigger();
}
/**
* 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()

View file

@ -4,6 +4,18 @@ use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
App::init()
->groups(['web'])
->inject('request')
->inject('response')
->action(function (Request $request, Response $response) {
$response
->addHeader('X-Frame-Options', 'SAMEORIGIN') // Avoid console and homepage from showing in iframes
->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url=' . \urlencode($request->getURI()))
->addHeader('X-UA-Compatible', 'IE=Edge') // Deny IE browsers from going into quirks mode
;
});
App::get('/console/*')
->alias('/')
->alias('auth/*')

View file

@ -6,7 +6,7 @@ use Utopia\Config\Config;
App::get('/versions')
->desc('Get Version')
->groups(['web', 'home'])
->groups(['home'])
->label('scope', 'public')
->inject('response')
->action(function (Response $response) {

View file

@ -34,6 +34,9 @@ use Appwrite\OpenSSL\OpenSSL;
use Appwrite\URL\URL as AppwriteURL;
use Utopia\App;
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\Helpers\ID;
use Utopia\Database\Database;
@ -59,9 +62,7 @@ use Utopia\Storage\Device\Linode;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Device\S3;
use Utopia\Storage\Device\Wasabi;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Adapter\MySQL;
use Utopia\Pools\Group;
@ -72,7 +73,6 @@ use Appwrite\Event\Func;
use MaxMind\Db\Reader;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Database\PDOProxy;
use Utopia\CLI\Console;
use Utopia\Queue;
use Utopia\Queue\Connection;
use Utopia\Storage\Storage;
@ -104,6 +104,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.8';
@ -245,6 +246,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');
@ -497,6 +499,29 @@ Database::addFilter(
}
);
Database::addFilter(
'userSearch',
function (mixed $value, Document $user) {
$searchValues = [
$user->getId(),
$user->getAttribute('email', ''),
$user->getAttribute('name', ''),
$user->getAttribute('phone', '')
];
foreach ($user->getAttribute('labels', []) as $label) {
$searchValues[] = 'label:' . $label;
}
$search = implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);
/**
* DB Formats
*/
@ -781,78 +806,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' => [
@ -878,6 +849,10 @@ App::setResource('loggerBreadcrumbs', function () {
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());
@ -1028,7 +1003,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Document $console */
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console'));
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', ''));
if ($projectId === 'console') {
return $console;
@ -1236,7 +1211,7 @@ App::setResource('schema', function ($utopia, $dbForProject) {
$complexity = function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []);
$query = Query::getByType($queries, Query::TYPE_LIMIT)[0] ?? null;
$query = Query::getByType($queries, [Query::TYPE_LIMIT])[0] ?? null;
$limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT;
return $complexity * $limit;

View file

@ -88,6 +88,7 @@ services:
- _APP_CONSOLE_WHITELIST_EMAILS
- _APP_CONSOLE_WHITELIST_IPS
- _APP_CONSOLE_INVITES
- _APP_CONSOLE_ROOT_SESSION
- _APP_SYSTEM_EMAIL_NAME
- _APP_SYSTEM_EMAIL_ADDRESS
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS

View file

@ -7,7 +7,7 @@ use Appwrite\Resque\Worker;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Exception as DatabaseException;
require_once __DIR__ . '/../init.php';
@ -29,11 +29,11 @@ class DatabaseV1 extends Worker
$database = new Document($this->args['database'] ?? []);
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
throw new DatabaseException('Missing collection');
}
if ($document->isEmpty()) {
throw new Exception('Missing document');
throw new DatabaseException('Missing document');
}
switch (strval($type)) {
@ -101,7 +101,7 @@ class DatabaseV1 extends Worker
case Database::VAR_RELATIONSHIP:
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
if ($relatedCollection->isEmpty()) {
throw new Exception('Collection not found');
throw new DatabaseException('Collection not found');
}
if (
@ -115,7 +115,7 @@ class DatabaseV1 extends Worker
onDelete: $options['onDelete'],
)
) {
throw new Exception('Failed to create Attribute');
throw new DatabaseException('Failed to create Attribute');
}
if ($options['twoWay']) {
@ -130,13 +130,28 @@ class DatabaseV1 extends Worker
}
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'available'));
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'failed'));
} catch (\Exception $e) {
Console::error($e->getMessage());
if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) {
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'failed'));
if ($e instanceof DatabaseException) {
$attribute->setAttribute('error', $e->getMessage());
if (isset($relatedAttribute)) {
$relatedAttribute->setAttribute('error', $e->getMessage());
}
}
$dbForProject->updateDocument(
'attributes',
$attribute->getId(),
$attribute->setAttribute('status', 'failed')
);
if (isset($relatedAttribute)) {
$dbForProject->updateDocument(
'attributes',
$relatedAttribute->getId(),
$relatedAttribute->setAttribute('status', 'failed')
);
}
} finally {
$target = Realtime::fromPayload(
@ -202,21 +217,21 @@ class DatabaseV1 extends Worker
try {
if ($status !== 'failed') {
if ($type === Database::VAR_RELATIONSHIP) {
if ($type === Database::VAR_RELATIONSHIP) {
if ($options['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
if ($relatedCollection->isEmpty()) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
throw new DatabaseException('Collection not found');
}
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
}
if (!$dbForProject->deleteRelationship('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'stuck'));
throw new Exception('Failed to delete Relationship');
throw new DatabaseException('Failed to delete Relationship');
}
} elseif (!$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new Exception('Failed to delete Attribute');
throw new DatabaseException('Failed to delete Attribute');
}
}
@ -225,9 +240,27 @@ class DatabaseV1 extends Worker
if (!$relatedAttribute->isEmpty()) {
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
}
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'stuck'));
} catch (\Exception $e) {
Console::error($e->getMessage());
if ($e instanceof DatabaseException) {
$attribute->setAttribute('error', $e->getMessage());
if (!$relatedAttribute->isEmpty()) {
$relatedAttribute->setAttribute('error', $e->getMessage());
}
}
$dbForProject->updateDocument(
'attributes',
$attribute->getId(),
$attribute->setAttribute('status', 'stuck')
);
if (!$relatedAttribute->isEmpty()) {
$dbForProject->updateDocument(
'attributes',
$relatedAttribute->getId(),
$relatedAttribute->setAttribute('status', 'stuck')
);
}
} finally {
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
@ -277,8 +310,7 @@ class DatabaseV1 extends Worker
$index
->setAttribute('attributes', $attributes, Document::SET_TYPE_ASSIGN)
->setAttribute('lengths', $lengths, Document::SET_TYPE_ASSIGN)
->setAttribute('orders', $orders, Document::SET_TYPE_ASSIGN)
;
->setAttribute('orders', $orders, Document::SET_TYPE_ASSIGN);
// Check if an index exists with the same attributes and orders
$exists = false;
@ -316,6 +348,7 @@ class DatabaseV1 extends Worker
* @param Document $collection
* @param Document $index
* @param Document $project
* @throws \Exception
*/
protected function createIndex(Document $database, Document $collection, Document $index, Document $project): void
{
@ -338,12 +371,20 @@ class DatabaseV1 extends Worker
try {
if (!$dbForProject->createIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $attributes, $lengths, $orders)) {
throw new Exception('Failed to create Index');
throw new DatabaseException('Failed to create Index');
}
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available'));
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'failed'));
} catch (\Exception $e) {
Console::error($e->getMessage());
if ($e instanceof DatabaseException) {
$index->setAttribute('error', $e->getMessage());
}
$dbForProject->updateDocument(
'indexes',
$index->getId(),
$index->setAttribute('status', 'failed')
);
} finally {
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
@ -392,12 +433,20 @@ class DatabaseV1 extends Worker
try {
if ($status !== 'failed' && !$dbForProject->deleteIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new Exception('Failed to delete index');
throw new DatabaseException('Failed to delete index');
}
$dbForProject->deleteDocument('indexes', $index->getId());
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'stuck'));
} catch (\Exception $e) {
Console::error($e->getMessage());
if ($e instanceof DatabaseException) {
$index->setAttribute('error', $e->getMessage());
}
$dbForProject->updateDocument(
'indexes',
$index->getId(),
$index->setAttribute('status', 'stuck')
);
} finally {
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern

View file

@ -351,6 +351,7 @@ class DeletesV1 extends Worker
foreach ($projects as $project) {
$this->deleteProject($project);
$dbForConsole->deleteDocument('projects', $project->getId());
}
}

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
{
}

View file

@ -41,15 +41,15 @@
"ext-openssl": "*",
"ext-zlib": "*",
"ext-sockets": "*",
"appwrite/php-clamav": "1.1.*",
"appwrite/php-clamav": "2.0.*",
"appwrite/php-runtimes": "0.11.*",
"utopia-php/abuse": "0.25.*",
"utopia-php/abuse": "0.27.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "0.27.*",
"utopia-php/audit": "0.29.*",
"utopia-php/cache": "0.8.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.36.*",
"utopia-php/database": "0.38.*",
"utopia-php/domains": "1.1.*",
"utopia-php/dsn": "0.1.*",
"utopia-php/framework": "0.28.*",
@ -67,10 +67,10 @@
"utopia-php/swoole": "0.5.*",
"utopia-php/websocket": "0.1.*",
"resque/php-resque": "1.3.6",
"matomo/device-detector": "6.0.*",
"dragonmantank/cron-expression": "3.3.1",
"phpmailer/phpmailer": "6.6.0",
"chillerlan/php-qrcode": "4.3.3",
"matomo/device-detector": "6.1.*",
"dragonmantank/cron-expression": "3.3.2",
"phpmailer/phpmailer": "6.8.0",
"chillerlan/php-qrcode": "4.3.4",
"adhocore/jwt": "1.1.2",
"webonyx/graphql-php": "14.11.*",
"slickdeals/statsd": "3.1.0",
@ -83,7 +83,7 @@
}
],
"require-dev": {
"appwrite/sdk-generator": "0.32.*",
"appwrite/sdk-generator": "0.33.*",
"ext-fileinfo": "*",
"phpunit/phpunit": "9.5.20",
"squizlabs/php_codesniffer": "^3.6",

138
composer.lock generated
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": "f41cf1d75b2967997bce80608ff9b603",
"content-hash": "f9e35e81b051baad38f0cb0e8fa611fe",
"packages": [
{
"name": "adhocore/jwt",
@ -65,24 +65,24 @@
},
{
"name": "appwrite/php-clamav",
"version": "1.1.0",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/php-clamav.git",
"reference": "61d00f24f9e7766fbba233e7b8d09c5475388073"
"reference": "f3897169f5c1f365312238a516ae9465f804634f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/php-clamav/zipball/61d00f24f9e7766fbba233e7b8d09c5475388073",
"reference": "61d00f24f9e7766fbba233e7b8d09c5475388073",
"url": "https://api.github.com/repos/appwrite/php-clamav/zipball/f3897169f5c1f365312238a516ae9465f804634f",
"reference": "f3897169f5c1f365312238a516ae9465f804634f",
"shasum": ""
},
"require": {
"ext-sockets": "*",
"php": ">=7.1"
"php": ">=8.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
"phpunit/phpunit": "^9"
},
"type": "library",
"autoload": {
@ -109,9 +109,9 @@
],
"support": {
"issues": "https://github.com/appwrite/php-clamav/issues",
"source": "https://github.com/appwrite/php-clamav/tree/1.1.0"
"source": "https://github.com/appwrite/php-clamav/tree/2.0.0"
},
"time": "2020-10-02T05:23:46+00:00"
"time": "2023-02-24T09:50:42+00:00"
},
{
"name": "appwrite/php-runtimes",
@ -158,20 +158,20 @@
},
{
"name": "chillerlan/php-qrcode",
"version": "4.3.3",
"version": "4.3.4",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-qrcode.git",
"reference": "6356b246948ac1025882b3f55e7c68ebd4515ae3"
"reference": "2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/6356b246948ac1025882b3f55e7c68ebd4515ae3",
"reference": "6356b246948ac1025882b3f55e7c68ebd4515ae3",
"url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d",
"reference": "2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d",
"shasum": ""
},
"require": {
"chillerlan/php-settings-container": "^2.1",
"chillerlan/php-settings-container": "^2.1.4",
"ext-mbstring": "*",
"php": "^7.4 || ^8.0"
},
@ -220,7 +220,7 @@
],
"support": {
"issues": "https://github.com/chillerlan/php-qrcode/issues",
"source": "https://github.com/chillerlan/php-qrcode/tree/4.3.3"
"source": "https://github.com/chillerlan/php-qrcode/tree/4.3.4"
},
"funding": [
{
@ -232,7 +232,7 @@
"type": "ko_fi"
}
],
"time": "2021-11-25T22:38:09+00:00"
"time": "2022-07-25T09:12:45+00:00"
},
{
"name": "chillerlan/php-settings-container",
@ -420,16 +420,16 @@
},
{
"name": "dragonmantank/cron-expression",
"version": "v3.3.1",
"version": "v3.3.2",
"source": {
"type": "git",
"url": "https://github.com/dragonmantank/cron-expression.git",
"reference": "be85b3f05b46c39bbc0d95f6c071ddff669510fa"
"reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/be85b3f05b46c39bbc0d95f6c071ddff669510fa",
"reference": "be85b3f05b46c39bbc0d95f6c071ddff669510fa",
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/782ca5968ab8b954773518e9e49a6f892a34b2a8",
"reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8",
"shasum": ""
},
"require": {
@ -469,7 +469,7 @@
],
"support": {
"issues": "https://github.com/dragonmantank/cron-expression/issues",
"source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.1"
"source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.2"
},
"funding": [
{
@ -477,7 +477,7 @@
"type": "github"
}
],
"time": "2022-01-18T15:43:28+00:00"
"time": "2022-09-10T18:51:20+00:00"
},
{
"name": "jean85/pretty-package-versions",
@ -686,16 +686,16 @@
},
{
"name": "matomo/device-detector",
"version": "6.0.6",
"version": "6.1.4",
"source": {
"type": "git",
"url": "https://github.com/matomo-org/device-detector.git",
"reference": "ce5ef5e6776c16af306d38e20674973f072e05ed"
"reference": "74f6c4f6732b3ad6cdf25560746841d522969112"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/matomo-org/device-detector/zipball/ce5ef5e6776c16af306d38e20674973f072e05ed",
"reference": "ce5ef5e6776c16af306d38e20674973f072e05ed",
"url": "https://api.github.com/repos/matomo-org/device-detector/zipball/74f6c4f6732b3ad6cdf25560746841d522969112",
"reference": "74f6c4f6732b3ad6cdf25560746841d522969112",
"shasum": ""
},
"require": {
@ -751,7 +751,7 @@
"source": "https://github.com/matomo-org/matomo",
"wiki": "https://dev.matomo.org/"
},
"time": "2023-01-16T08:18:02+00:00"
"time": "2023-08-02T08:48:53+00:00"
},
{
"name": "mongodb/mongodb",
@ -873,16 +873,16 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v6.6.0",
"version": "v6.8.0",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "e43bac82edc26ca04b36143a48bde1c051cfd5b1"
"reference": "df16b615e371d81fb79e506277faea67a1be18f1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e43bac82edc26ca04b36143a48bde1c051cfd5b1",
"reference": "e43bac82edc26ca04b36143a48bde1c051cfd5b1",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/df16b615e371d81fb79e506277faea67a1be18f1",
"reference": "df16b615e371d81fb79e506277faea67a1be18f1",
"shasum": ""
},
"require": {
@ -892,22 +892,24 @@
"php": ">=5.5.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
"doctrine/annotations": "^1.2",
"php-parallel-lint/php-console-highlighter": "^0.5.0",
"php-parallel-lint/php-parallel-lint": "^1.3.1",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.2",
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.6.2",
"yoast/phpunit-polyfills": "^1.0.0"
"squizlabs/php_codesniffer": "^3.7.1",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)"
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"type": "library",
"autoload": {
@ -939,7 +941,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.0"
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.0"
},
"funding": [
{
@ -947,7 +949,7 @@
"type": "github"
}
],
"time": "2022-02-28T15:31:21+00:00"
"time": "2023-03-06T14:43:22+00:00"
},
{
"name": "psr/log",
@ -1223,23 +1225,23 @@
},
{
"name": "utopia-php/abuse",
"version": "0.25.0",
"version": "0.27.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "49a180cab5316cddec9676d900d5112d03e97ffc"
"reference": "d1115f5843e903ffaba9c23e450b33c0fe265ae0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/49a180cab5316cddec9676d900d5112d03e97ffc",
"reference": "49a180cab5316cddec9676d900d5112d03e97ffc",
"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.36.*"
"utopia-php/database": "0.38.*"
},
"require-dev": {
"laravel/pint": "1.5.*",
@ -1266,9 +1268,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/0.25.0"
"source": "https://github.com/utopia-php/abuse/tree/0.27.0"
},
"time": "2023-04-27T15:43:47+00:00"
"time": "2023-07-15T00:53:50+00:00"
},
{
"name": "utopia-php/analytics",
@ -1318,21 +1320,21 @@
},
{
"name": "utopia-php/audit",
"version": "0.27.0",
"version": "0.29.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "bdf89d7fe381bd4c891ad217612580a35e8c7642"
"reference": "5318538f457bf73623629345c98ea06371ca5dd4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/bdf89d7fe381bd4c891ad217612580a35e8c7642",
"reference": "bdf89d7fe381bd4c891ad217612580a35e8c7642",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/5318538f457bf73623629345c98ea06371ca5dd4",
"reference": "5318538f457bf73623629345c98ea06371ca5dd4",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/database": "0.36.*"
"utopia-php/database": "0.38.*"
},
"require-dev": {
"laravel/pint": "1.5.*",
@ -1359,9 +1361,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.27.0"
"source": "https://github.com/utopia-php/audit/tree/0.29.0"
},
"time": "2023-05-15T07:04:48+00:00"
"time": "2023-07-15T00:51:10+00:00"
},
{
"name": "utopia-php/cache",
@ -1514,16 +1516,16 @@
},
{
"name": "utopia-php/database",
"version": "0.36.1",
"version": "0.38.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "f6ab65e59a199da5155c114564077b1ab8c4daef"
"reference": "59e4684cf87e03c12dab9240158c1dfc6888e534"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/f6ab65e59a199da5155c114564077b1ab8c4daef",
"reference": "f6ab65e59a199da5155c114564077b1ab8c4daef",
"url": "https://api.github.com/repos/utopia-php/database/zipball/59e4684cf87e03c12dab9240158c1dfc6888e534",
"reference": "59e4684cf87e03c12dab9240158c1dfc6888e534",
"shasum": ""
},
"require": {
@ -1534,8 +1536,6 @@
"utopia-php/mongo": "0.2.*"
},
"require-dev": {
"ext-mongodb": "*",
"ext-redis": "*",
"fakerphp/faker": "^1.14",
"laravel/pint": "1.4.*",
"mongodb/mongodb": "1.8.0",
@ -1566,9 +1566,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.36.1"
"source": "https://github.com/utopia-php/database/tree/0.38.0"
},
"time": "2023-04-27T08:39:55+00:00"
"time": "2023-07-14T07:49:38+00:00"
},
{
"name": "utopia-php/domains",
@ -2642,16 +2642,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.32.3",
"version": "0.33.7",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "4057e14a61335070034b1cbdce9e39bef94d997d"
"reference": "9f5db4a637b23879ceacea9ed2d33b0486771ffc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/4057e14a61335070034b1cbdce9e39bef94d997d",
"reference": "4057e14a61335070034b1cbdce9e39bef94d997d",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9f5db4a637b23879ceacea9ed2d33b0486771ffc",
"reference": "9f5db4a637b23879ceacea9ed2d33b0486771ffc",
"shasum": ""
},
"require": {
@ -2687,9 +2687,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.32.3"
"source": "https://github.com/appwrite/sdk-generator/tree/0.33.7"
},
"time": "2023-04-27T19:22:05+00:00"
"time": "2023-07-12T12:15:43+00:00"
},
{
"name": "doctrine/deprecations",

View file

@ -104,6 +104,7 @@ services:
- _APP_CONSOLE_WHITELIST_CODES
- _APP_CONSOLE_WHITELIST_IPS
- _APP_CONSOLE_INVITES
- _APP_CONSOLE_ROOT_SESSION
- _APP_SYSTEM_EMAIL_NAME
- _APP_SYSTEM_EMAIL_ADDRESS
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS

View file

@ -0,0 +1,12 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
Token result = await account.CreatePhoneVerification();

View file

@ -0,0 +1,14 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
Token result = await account.CreateRecovery(
email: "email@example.com",
url: "https://example.com");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
Token result = await account.CreateVerification(
url: "https://example.com");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
await account.DeleteSession(
sessionId: "[SESSION_ID]");

View file

@ -0,0 +1,12 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
await account.DeleteSessions();

View file

@ -0,0 +1,12 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
Preferences result = await account.GetPrefs();

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
Session result = await account.GetSession(
sessionId: "[SESSION_ID]");

View file

@ -0,0 +1,12 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
User result = await account.Get();

View file

@ -0,0 +1,12 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
LogList result = await account.ListLogs();

View file

@ -0,0 +1,12 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
SessionList result = await account.ListSessions();

View file

@ -0,0 +1,14 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
User result = await account.UpdateEmail(
email: "email@example.com",
password: "password");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
User result = await account.UpdateName(
name: "[NAME]");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
User result = await account.UpdatePassword(
password: "");

View file

@ -0,0 +1,14 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
Token result = await account.UpdatePhoneVerification(
userId: "[USER_ID]",
secret: "[SECRET]");

View file

@ -0,0 +1,14 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
User result = await account.UpdatePhone(
phone: "+12065550100",
password: "password");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
User result = await account.UpdatePrefs(
prefs: [object]);

View file

@ -0,0 +1,16 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
Token result = await account.UpdateRecovery(
userId: "[USER_ID]",
secret: "[SECRET]",
password: "password",
passwordAgain: "password");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
Session result = await account.UpdateSession(
sessionId: "[SESSION_ID]");

View file

@ -0,0 +1,12 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
User result = await account.UpdateStatus();

View file

@ -0,0 +1,14 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..."); // Your secret JSON Web Token
var account = new Account(client);
Token result = await account.UpdateVerification(
userId: "[USER_ID]",
secret: "[SECRET]");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var avatars = new Avatars(client);
byte[] result = await avatars.GetBrowser(
code: "aa");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var avatars = new Avatars(client);
byte[] result = await avatars.GetCreditCard(
code: "amex");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var avatars = new Avatars(client);
byte[] result = await avatars.GetFavicon(
url: "https://example.com");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var avatars = new Avatars(client);
byte[] result = await avatars.GetFlag(
code: "af");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var avatars = new Avatars(client);
byte[] result = await avatars.GetImage(
url: "https://example.com");

View file

@ -0,0 +1,12 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var avatars = new Avatars(client);
byte[] result = await avatars.GetInitials();

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var avatars = new Avatars(client);
byte[] result = await avatars.GetQR(
text: "[TEXT]");

View file

@ -0,0 +1,16 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeBoolean result = await databases.CreateBooleanAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
required: false);

View file

@ -0,0 +1,15 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
Collection result = await databases.CreateCollection(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
name: "[NAME]");

View file

@ -0,0 +1,16 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeDatetime result = await databases.CreateDatetimeAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
required: false);

View file

@ -0,0 +1,16 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
Document result = await databases.CreateDocument(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
documentId: "[DOCUMENT_ID]",
data: [object]);

View file

@ -0,0 +1,16 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeEmail result = await databases.CreateEmailAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
required: false);

View file

@ -0,0 +1,17 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeEnum result = await databases.CreateEnumAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
elements: new List<string> {},
required: false);

View file

@ -0,0 +1,16 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeFloat result = await databases.CreateFloatAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
required: false);

View file

@ -0,0 +1,17 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
Index result = await databases.CreateIndex(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
type: "key",
attributes: new List<string> {});

View file

@ -0,0 +1,16 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeInteger result = await databases.CreateIntegerAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
required: false);

View file

@ -0,0 +1,16 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeIp result = await databases.CreateIpAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
required: false);

View file

@ -0,0 +1,16 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeRelationship result = await databases.CreateRelationshipAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
relatedCollectionId: "[RELATED_COLLECTION_ID]",
type: "oneToOne");

View file

@ -0,0 +1,17 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeString result = await databases.CreateStringAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
size: 1,
required: false);

View file

@ -0,0 +1,16 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeUrl result = await databases.CreateUrlAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
required: false);

View file

@ -0,0 +1,14 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
Database result = await databases.Create(
databaseId: "[DATABASE_ID]",
name: "[NAME]");

View file

@ -0,0 +1,15 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
await databases.DeleteAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "");

View file

@ -0,0 +1,14 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
await databases.DeleteCollection(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]");

View file

@ -0,0 +1,15 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
await databases.DeleteDocument(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
documentId: "[DOCUMENT_ID]");

View file

@ -0,0 +1,15 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
await databases.DeleteIndex(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
await databases.Delete(
databaseId: "[DATABASE_ID]");

View file

@ -0,0 +1,15 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
result = await databases.GetAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "");

View file

@ -0,0 +1,14 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
Collection result = await databases.GetCollection(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]");

View file

@ -0,0 +1,15 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
Document result = await databases.GetDocument(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
documentId: "[DOCUMENT_ID]");

View file

@ -0,0 +1,15 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
Index result = await databases.GetIndex(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
Database result = await databases.Get(
databaseId: "[DATABASE_ID]");

View file

@ -0,0 +1,14 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeList result = await databases.ListAttributes(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]");

View file

@ -0,0 +1,13 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
CollectionList result = await databases.ListCollections(
databaseId: "[DATABASE_ID]");

View file

@ -0,0 +1,14 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
DocumentList result = await databases.ListDocuments(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]");

View file

@ -0,0 +1,14 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
IndexList result = await databases.ListIndexes(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]");

View file

@ -0,0 +1,12 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
DatabaseList result = await databases.List();

View file

@ -0,0 +1,17 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeBoolean result = await databases.UpdateBooleanAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
required: false,
default: false);

View file

@ -0,0 +1,15 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
Collection result = await databases.UpdateCollection(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
name: "[NAME]");

View file

@ -0,0 +1,17 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
AttributeDatetime result = await databases.UpdateDatetimeAttribute(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
key: "",
required: false,
default: "");

View file

@ -0,0 +1,15 @@
using Appwrite;
using Appwrite.Services;
using Appwrite.Models;
var client = new Client()
.SetEndPoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("5df5acd0d48c2") // Your project ID
.SetKey("919c2d18fb5d4...a2ae413da83346ad2"); // Your secret API key
var databases = new Databases(client);
Document result = await databases.UpdateDocument(
databaseId: "[DATABASE_ID]",
collectionId: "[COLLECTION_ID]",
documentId: "[DOCUMENT_ID]");

Some files were not shown because too many files have changed in this diff Show more