Compare commits

..
Author SHA1 Message Date
loks0n 2cb86c57ef fix: preflight requests 2025-12-12 09:49:04 +00:00
Luke B. SilverandGitHub e4a7b1b103 Merge pull request #10937 from appwrite/fix-imagine-hostnames-2
Fix imagine hostnames 2
2025-12-11 19:05:12 +00:00
loks0n c6ffa181a2 fix: imagine hostnames 2 2025-12-11 18:53:58 +00:00
Matej BačoandGitHub cd8e93db60 Merge pull request #10031 from appwrite/chore-add-new-site-templates
Add formspree and react-admin templates to sites
2025-12-11 16:56:03 +01:00
Aditya Oberai ceb2108012 shift db seeding command to build command 2025-12-11 15:00:59 +00:00
Aditya Oberai 383f0c1ef1 remove unnecessary images 2025-12-11 15:00:04 +00:00
Matej Bačo 52ccbfcdf7 AI code quality fixes 2025-12-11 15:52:44 +01:00
Aditya OberaiandGitHub d502ab0cd5 Merge branch '1.8.x' into chore-add-new-site-templates 2025-12-11 18:35:52 +05:30
Matej Bačo e738bae523 Fix screenshot script; auto-fill API key, custom required env vars
Tag is correct fallback: https://github.com/appwrite/appwrite/blob/decf2c7a1bc5d2d989434fb0adc6a6fb44a77db2/src/Appwrite/Utopia/Request/Filters/V21.php#L28
2025-12-11 10:31:05 +01:00
Aditya Oberai decf2c7a1b add new site templates for CRM dashboard and job applications form 2025-12-05 18:51:22 +00:00
Aditya Oberai 43d57bbe4e Merge remote-tracking branch 'origin/1.8.x' into chore-add-new-site-templates 2025-12-05 16:19:08 +00:00
Aditya OberaiandGitHub 27ede0079f Merge branch '1.7.x' into chore-add-new-site-templates 2025-12-05 02:58:06 +05:30
Jake BarnbyandGitHub a94d33649b Merge pull request #10493 from appwrite/fix-check-1.7.x
Move disable
2025-09-22 19:51:40 +12:00
Jake Barnby 6d25eb81b6 Move disable 2025-09-15 13:09:08 +12:00
Khushboo Verma 8d7c76a434 Add screenshots for 2 templates 2025-06-20 12:53:06 +05:30
Khushboo Verma c927a1f459 Merge branch '1.7.x' into chore-add-new-site-templates 2025-06-20 12:09:34 +05:30
Khushboo Verma 88239a23f9 Update env vars for react-admin 2025-06-20 12:07:24 +05:30
Khushboo Verma 651af6a2c5 Add formspree, clerk and react-admin templates to sites 2025-06-19 21:02:31 +05:30
32 changed files with 326 additions and 258 deletions
-1
View File
@@ -125,4 +125,3 @@ _APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
_APP_PROJECT_REGIONS=default
_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000
_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
_APP_TRUSTED_HEADERS=
-2
View File
@@ -164,7 +164,6 @@ Getting started with Appwrite is as easy as creating a new project, choosing you
| | [Quick start for PHP](https://appwrite.io/docs/quick-starts/php) |
| | [Quick start for Kotlin](https://appwrite.io/docs/quick-starts/kotlin) |
| | [Quick start for Swift](https://appwrite.io/docs/quick-starts/swift) |
| | [Quick start for Go](https://appwrite.io/docs/quick-starts/go) |
### Products
@@ -206,7 +205,6 @@ Below is a list of currently supported platforms and languages. If you would lik
- :white_check_mark:   [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (Maintained by the Appwrite Team)
- :white_check_mark:   [Swift](https://github.com/appwrite/sdk-for-swift) (Maintained by the Appwrite Team)
- :white_check_mark:   [.NET](https://github.com/appwrite/sdk-for-dotnet) - **Beta** (Maintained by the Appwrite Team)
- :white_check_mark:   [Go](https://github.com/appwrite/sdk-for-go) - **Beta** (Maintained by the Appwrite Team)
#### Community
+6 -2
View File
@@ -6,8 +6,12 @@ use Utopia\System\System;
* Platform configuration
*/
return [
'domain' => System::getEnv('_APP_DOMAIN', 'localhost'),
'consoleDomain' => System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', 'localhost')),
'apiHostname' => System::getEnv('_APP_DOMAIN', 'localhost'),
'consoleHostname' => System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', 'localhost')),
'hostnames' => array_filter(array_unique([
System::getEnv('_APP_DOMAIN', 'localhost'),
System::getEnv('_APP_CONSOLE_DOMAIN', 'localhost'),
])),
'platformName' => APP_EMAIL_PLATFORM_NAME,
'logoUrl' => APP_EMAIL_LOGO_URL,
'accentColor' => APP_EMAIL_ACCENT_COLOR,
+134 -1
View File
@@ -9,7 +9,7 @@ use Utopia\System\System;
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$platform = Config::getParam('platform', []);
$hostname = $platform['consoleDomain'] ?? '';
$hostname = $platform['consoleHostname'] ?? '';
$url = $protocol . '://' . $hostname;
@@ -22,6 +22,8 @@ class UseCases
public const DOCUMENTATION = 'documentation';
public const BLOG = 'blog';
public const AI = 'artificial intelligence';
public const FORMS = 'forms';
public const DASHBOARD = 'dashboard';
}
const TEMPLATE_FRAMEWORKS = [
@@ -1469,4 +1471,135 @@ return [
],
]
],
[
'key' => 'crm-dashboard-react-admin',
'name' => 'CRM dashboard with React Admin',
'tagline' => 'A React-based admin dashboard template with CRM features.',
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::DASHBOARD],
'screenshotDark' => $url . '/images/sites/templates/crm-dashboard-react-admin-dark.png',
'screenshotLight' => $url . '/images/sites/templates/crm-dashboard-react-admin-light.png',
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './react/react-admin',
'installCommand' => 'pnpm install',
'buildCommand' => 'pnpm build && pnpm db-seed',
'outputDirectory' => './dist',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.7.*',
'variables' => [
[
'name' => 'VITE_APPWRITE_ENDPOINT',
'description' => 'Endpoint of Appwrite server',
'value' => '{apiEndpoint}',
'placeholder' => '{apiEndpoint}',
'required' => true,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_PROJECT_ID',
'description' => 'Your Appwrite project ID',
'value' => '{projectId}',
'placeholder' => '{projectId}',
'required' => true,
'type' => 'text'
],
[
'name' => 'APPWRITE_API_KEY',
'description' => 'Your Appwrite API key (for seeding only)',
'value' => '',
'placeholder' => 'a0b1...',
'required' => true,
'type' => 'password'
],
[
'name' => 'VITE_APPWRITE_DATABASE_ID',
'description' => 'Database ID (default: admin)',
'value' => 'admin',
'placeholder' => 'admin',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_REVIEWS',
'description' => 'Table ID for reviews table',
'value' => 'reviews',
'placeholder' => 'reviews',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_INVOICES',
'description' => 'Table ID for invoices table',
'value' => 'invoices',
'placeholder' => 'invoices',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_ORDERS',
'description' => 'Table ID for orders table',
'value' => 'orders',
'placeholder' => 'orders',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_PRODUCTS',
'description' => 'Table ID for products table',
'value' => 'products',
'placeholder' => 'products',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_CATEGORIES',
'description' => 'Table ID for categories table',
'value' => 'categories',
'placeholder' => 'categories',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_CUSTOMERS',
'description' => 'Table ID for customers table',
'value' => 'customers',
'placeholder' => 'customers',
'required' => false,
'type' => 'text'
],
]
],
[
'key' => 'job-applications-formspree',
'name' => 'Job applications form with Formspree',
'tagline' => 'A simple form submission template using Formspree.',
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::FORMS],
'screenshotDark' => $url . '/images/sites/templates/job-applications-formspree-dark.png',
'screenshotLight' => $url . '/images/sites/templates/job-applications-formspree-light.png',
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './react/formspree',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.7.*',
'variables' => [
[
'name' => 'VITE_FORMSPREE_FORM_ID',
'description' => 'Your Formspree form ID',
'value' => '',
'placeholder' => 'xrgkpqld',
'required' => true,
'type' => 'text'
],
]
]
];
-9
View File
@@ -357,15 +357,6 @@ return [
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_TRUSTED_HEADERS',
'description' => 'This option allows you to set the list of trusted headers, the value is a commaseparated list of HTTP header names, evaluated left-to-right for the first valid IP. Header names are treated case-insensitively.',
'introduction' => '1.8.0',
'default' => 'x-forwarded-for',
'required' => false,
'question' => '',
'filter' => ''
]
],
],
+33 -45
View File
@@ -194,7 +194,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, Doc
}
;
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, string $trustedIp) {
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode) {
/** @var Appwrite\Utopia\Database\Documents\User $userFromRequest */
$userFromRequest = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId));
@@ -214,7 +214,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($trustedIp);
$record = $geodb->get($request->getIP());
$sessionSecret = $proofForToken->generate();
$factor = (match ($verifiedToken->getAttribute('type')) {
@@ -243,7 +243,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
'provider' => $provider,
'secret' => $proofForToken->hash($sessionSecret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
'factors' => [$factor],
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
@@ -358,13 +358,13 @@ App::post('/v1/account')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('request')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->inject('trustedIp')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $user, Document $project, Database $dbForProject, Hooks $hooks, string $trustedIp) {
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Hooks $hooks) {
$email = \strtolower($email);
if ('console' === $project->getId()) {
@@ -375,7 +375,7 @@ App::post('/v1/account')
throw new Exception(Exception::USER_EMAIL_NOT_WHITELISTED);
}
if (!empty($whitelistIPs) && !\in_array($trustedIp, $whitelistIPs)) {
if (!empty($whitelistIPs) && !\in_array($request->getIP(), $whitelistIPs)) {
throw new Exception(Exception::USER_IP_NOT_WHITELISTED);
}
}
@@ -961,8 +961,7 @@ App::post('/v1/account/sessions/email')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('trustedIp')
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, string $trustedIp) {
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) {
$email = \strtolower($email);
$protocol = $request->getProtocol();
@@ -986,7 +985,7 @@ App::post('/v1/account/sessions/email')
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($trustedIp);
$record = $geodb->get($request->getIP());
$secret = $proofForToken->generate();
$session = new Document(array_merge(
[
@@ -997,7 +996,7 @@ App::post('/v1/account/sessions/email')
'providerUid' => $email,
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
'factors' => ['password'],
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
@@ -1106,8 +1105,7 @@ App::post('/v1/account/sessions/anonymous')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('trustedIp')
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, string $trustedIp) {
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) {
$protocol = $request->getProtocol();
if ('console' === $project->getId()) {
@@ -1157,7 +1155,7 @@ App::post('/v1/account/sessions/anonymous')
// Create session token
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($trustedIp);
$record = $geodb->get($request->getIP());
$secret = $proofForToken->generate();
$session = new Document(array_merge(
@@ -1168,7 +1166,7 @@ App::post('/v1/account/sessions/anonymous')
'provider' => SESSION_PROVIDER_ANONYMOUS,
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
'factors' => ['anonymous'],
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
@@ -1259,7 +1257,6 @@ App::post('/v1/account/sessions/token')
->inject('store')
->inject('proofForToken')
->inject('proofForCode')
->inject('trustedIp')
->action($createSession);
App::get('/v1/account/sessions/oauth2/:provider')
@@ -1328,7 +1325,7 @@ App::get('/v1/account/sessions/oauth2/:provider')
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$host = $platform['consoleDomain'] ?? '';
$host = $platform['consoleHostname'] ?? '';
$redirectBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
$redirectBase .= ':' . $port;
@@ -1456,8 +1453,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('trustedIp')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, string $trustedIp) use ($oauthDefaultSuccess) {
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) use ($oauthDefaultSuccess) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
@@ -1820,7 +1816,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'secret' => $proofForTokenOAuth2->hash($secret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
@@ -1844,7 +1840,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
// If the `token` param is not set, we persist the session in a cookie
} else {
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($trustedIp);
$record = $geodb->get($request->getIP());
$secret = $proofForToken->generate();
$session = new Document(array_merge([
@@ -1858,7 +1854,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry),
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
'factors' => [TYPE::EMAIL, 'oauth2'], // include a special oauth2 factor to bypass MFA checks
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
@@ -1982,7 +1978,7 @@ App::get('/v1/account/tokens/oauth2/:provider')
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$host = $platform['consoleDomain'] ?? '';
$host = $platform['consoleHostname'] ?? '';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$redirectBase = $protocol . '://' . $host;
@@ -2051,8 +2047,7 @@ App::post('/v1/account/tokens/magic-url')
->inject('queueForMails')
->inject('proofForPassword')
->inject('platform')
->inject('trustedIp')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform, string $trustedIp) {
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2142,7 +2137,7 @@ App::post('/v1/account/tokens/magic-url')
'secret' => $proofForToken->hash($tokenSecret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
@@ -2158,7 +2153,7 @@ App::post('/v1/account/tokens/magic-url')
if (empty($url)) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$host = $platform['consoleDomain'] ?? '';
$host = $platform['consoleHostname'] ?? '';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
@@ -2320,8 +2315,7 @@ App::post('/v1/account/tokens/email')
->inject('queueForMails')
->inject('proofForPassword')
->inject('proofForCode')
->inject('trustedIp')
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode, string $trustedIp) {
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2427,7 +2421,7 @@ App::post('/v1/account/tokens/email')
'secret' => $proofForCode->hash($tokenSecret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
@@ -2613,11 +2607,10 @@ App::put('/v1/account/sessions/magic-url')
->inject('queueForMails')
->inject('store')
->inject('proofForCode')
->inject('trustedIp')
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $trustedIp) use ($createSession) {
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode) use ($createSession) {
$proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
$proofForToken->setHash(new Sha());
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $trustedIp);
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode);
});
App::put('/v1/account/sessions/phone')
@@ -2662,7 +2655,6 @@ App::put('/v1/account/sessions/phone')
->inject('store')
->inject('proofForToken')
->inject('proofForCode')
->inject('trustedIp')
->action($createSession);
App::post('/v1/account/tokens/phone')
@@ -2705,8 +2697,7 @@ App::post('/v1/account/tokens/phone')
->inject('plan')
->inject('store')
->inject('proofForCode')
->inject('trustedIp')
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode, string $trustedIp) {
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -2801,7 +2792,7 @@ App::post('/v1/account/tokens/phone')
'secret' => $proofForCode->hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
@@ -3477,8 +3468,7 @@ App::post('/v1/account/recovery')
->inject('queueForMails')
->inject('queueForEvents')
->inject('proofForToken')
->inject('trustedIp')
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken, string $trustedIp) {
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -3511,7 +3501,7 @@ App::post('/v1/account/recovery')
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($profile->getId())->toString());
@@ -3774,8 +3764,7 @@ App::post('/v1/account/verifications/email')
->inject('queueForEvents')
->inject('queueForMails')
->inject('proofForToken')
->inject('trustedIp')
->action(function (string $url, Request $request, Response $response, Document $project, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken, string $trustedIp) {
->action(function (string $url, Request $request, Response $response, Document $project, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -3801,7 +3790,7 @@ App::post('/v1/account/verifications/email')
'secret' => $proofForToken->hash($verificationSecret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
@@ -4063,8 +4052,7 @@ App::post('/v1/account/verifications/phone')
->inject('queueForStatsUsage')
->inject('plan')
->inject('proofForCode')
->inject('trustedIp')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode, string $trustedIp) {
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -4100,7 +4088,7 @@ App::post('/v1/account/verifications/phone')
'secret' => $proofForCode->hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
+2 -3
View File
@@ -32,12 +32,11 @@ App::get('/v1/locale')
->inject('response')
->inject('locale')
->inject('geodb')
->inject('trustedIp')
->action(function (Request $request, Response $response, Locale $locale, Reader $geodb, string $trustedIp) {
->action(function (Request $request, Response $response, Locale $locale, Reader $geodb) {
$eu = Config::getParam('locale-eu');
$currencies = Config::getParam('locale-currencies');
$output = [];
$ip = $trustedIp;
$ip = $request->getIP();
$output['ip'] = $ip;
+3 -4
View File
@@ -1209,8 +1209,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->inject('queueForEvents')
->inject('store')
->inject('proofForToken')
->inject('trustedIp')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, string $trustedIp) {
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
$protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId);
@@ -1262,7 +1261,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
Authorization::setRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($trustedIp);
$record = $geodb->get($request->getIP());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = $proofForToken->generate();
@@ -1279,7 +1278,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
'providerUid' => $user->getAttribute('email'),
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp,
'ip' => $request->getIP(),
'factors' => ['email'],
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
+5 -7
View File
@@ -2216,8 +2216,7 @@ App::post('/v1/users/:userId/sessions')
->inject('queueForEvents')
->inject('store')
->inject('proofForToken')
->inject('trustedIp')
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, string $trustedIp) {
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -2225,7 +2224,7 @@ App::post('/v1/users/:userId/sessions')
$secret = $proofForToken->generate();
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($trustedIp);
$record = $geodb->get($request->getIP());
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
@@ -2239,7 +2238,7 @@ App::post('/v1/users/:userId/sessions')
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'factors' => ['server'],
'ip' => $trustedIp,
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => $expire,
],
@@ -2306,8 +2305,7 @@ App::post('/v1/users/:userId/tokens')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('trustedIp')
->action(function (string $userId, int $length, int $expire, Request $request, Response $response, Database $dbForProject, Event $queueForEvents, string $trustedIp) {
->action(function (string $userId, int $length, int $expire, Request $request, Response $response, Database $dbForProject, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
@@ -2327,7 +2325,7 @@ App::post('/v1/users/:userId/tokens')
'secret' => $proofForToken->hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $trustedIp
'ip' => $request->getIP()
]);
$token = $dbForProject->createDocument('tokens', $token);
+3 -3
View File
@@ -132,7 +132,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$commentStatus = $isAuthorized ? 'waiting' : 'failed';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleDomain'] ?? '';
$hostname = $platform['consoleHostname'] ?? '';
$authorizeUrl = $protocol . '://' . $hostname . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}";
@@ -555,7 +555,7 @@ App::get('/v1/vcs/github/authorize')
$appName = System::getEnv('_APP_VCS_GITHUB_APP_NAME');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleDomain'] ?? '';
$hostname = $platform['consoleHostname'] ?? '';
if (empty($appName)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'GitHub App name is not configured. Please configure VCS (Version Control System) variables in .env file.');
@@ -614,7 +614,7 @@ App::get('/v1/vcs/github/callback')
$region = $project->getAttribute('region', 'default');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleDomain'] ?? '';
$hostname = $platform['consoleHostname'] ?? '';
$defaultState = [
'success' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
+28 -27
View File
@@ -59,7 +59,7 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, array $domains)
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
@@ -80,7 +80,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$errorView = __DIR__ . '/../views/general/error.phtml';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$url = $protocol . '://' . $platform['consoleDomain'];
$url = $protocol . '://' . $platform['consoleHostname'];
$platformHostnames = $platform['hostnames'] ?? [];
if ($rule->isEmpty()) {
$appDomainFunctionsFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
@@ -101,7 +102,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
throw $exception;
}
if (!in_array($host, $domains)) {
if (!in_array($host, $platformHostnames)) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
}
@@ -269,7 +270,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
if (!$authorized) {
$url = $protocol . "://" . $platform['consoleDomain'];
$url = $protocol . "://" . $platform['consoleHostname'];
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
@@ -366,7 +367,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
$ip = $trustedIp;
$ip = $request->getIP();
$headers['x-appwrite-client-ip'] = $ip;
$jwtExpiry = $resource->getAttribute('timeout', 900) + 60; // 1min extra to account for possible cold-starts
@@ -858,15 +859,15 @@ App::init()
->inject('devKey')
->inject('apiKey')
->inject('cors')
->inject('domains')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, array $domains) {
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors) {
/*
* Appwrite Router
*/
$hostname = $request->getHostname() ?? '';
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($hostname, $domains) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey, $domains)) {
if (!in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1028,10 +1029,11 @@ App::init()
->inject('console')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('domains')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $domains) {
->inject('platform')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform) {
$hostname = $request->getHostname();
$cache = Config::getParam('domains', []);
$cache = Config::getParam('hostnames', []);
$platformHostnames = $platform['hostnames'] ?? [];
// 1. Cache hit
if (array_key_exists($hostname, $cache)) {
@@ -1042,7 +1044,7 @@ App::init()
$domain = new Domain(!empty($hostname) ? $hostname : '');
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
$cache[$domain->get()] = false;
Config::setParam('domains', $cache);
Config::setParam('hostnames', $cache);
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
return;
}
@@ -1053,7 +1055,7 @@ App::init()
}
// 3. Check if domain is a main domain
if (!in_array($domain->get(), $domains)) {
if (!in_array($domain->get(), $platformHostnames)) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
return;
}
@@ -1114,7 +1116,7 @@ App::init()
Console::info('Certificate already exists');
} finally {
$cache[$domain->get()] = true;
Config::setParam('domains', $cache);
Config::setParam('hostnames', $cache);
Authorization::reset();
}
});
@@ -1138,14 +1140,14 @@ App::options()
->inject('project')
->inject('devKey')
->inject('apiKey')
->inject('domains')
->inject('cors')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, array $domains, Cors $cors) {
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors) {
/*
* Appwrite Router
*/
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($request->getHostname(), $domains) || !empty($previewHostname)) {
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
@@ -1179,9 +1181,8 @@ App::error()
->inject('logger')
->inject('log')
->inject('queueForStatsUsage')
->inject('trustedIp')
->inject('devKey')
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage, string $trustedIp) {
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
$class = \get_class($error);
@@ -1292,7 +1293,7 @@ App::error()
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
} else {
$log->setUser(new User('guest-' . hash('sha256', $trustedIp)));
$log->setUser(new User('guest-' . hash('sha256', $request->getIP())));
}
try {
@@ -1449,9 +1450,9 @@ App::get('/robots.txt')
->inject('platform')
->inject('previewHostname')
->inject('apiKey')
->inject('domains')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, array $domains) {
if (in_array($request->getHostname(), $domains) || !empty($previewHostname)) {
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
@@ -1481,9 +1482,9 @@ App::get('/humans.txt')
->inject('platform')
->inject('previewHostname')
->inject('apiKey')
->inject('domains')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, array $domains) {
if (in_array($request->getHostname(), $domains) || !empty($previewHostname)) {
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
+3 -4
View File
@@ -509,8 +509,7 @@ App::init()
->inject('devKey')
->inject('telemetry')
->inject('platform')
->inject('trustedIp')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Migration $queueForMigrations, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, string $trustedIp) use ($usageDatabaseListener, $eventDatabaseListener) {
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Migration $queueForMigrations, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform) use ($usageDatabaseListener, $eventDatabaseListener) {
$route = $utopia->getRoute();
@@ -538,7 +537,7 @@ App::init()
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $trustedIp)
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
@@ -599,7 +598,7 @@ App::init()
$queueForAudits
->setMode($mode)
->setUserAgent($request->getUserAgent(''))
->setIP($trustedIp)
->setIP($request->getIP())
->setHostname($request->getHostname())
->setEvent($route->getLabel('audits.event', ''))
->setProject($project);
+2 -3
View File
@@ -36,12 +36,11 @@ App::init()
->inject('request')
->inject('project')
->inject('geodb')
->inject('trustedIp')
->action(function (App $utopia, Request $request, Document $project, Reader $geodb, string $trustedIp) {
->action(function (App $utopia, Request $request, Document $project, Reader $geodb) {
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
if (!empty($denylist && $project->getId() === 'console')) {
$countries = explode(',', $denylist);
$record = $geodb->get($trustedIp) ?? [];
$record = $geodb->get($request->getIP()) ?? [];
$country = $record['country']['iso_code'] ?? '';
if (in_array($country, $countries)) {
throw new Exception(Exception::GENERAL_REGION_ACCESS_DENIED);
+1 -2
View File
@@ -2,7 +2,6 @@
require_once __DIR__ . '/../vendor/autoload.php';
use Apppwrite\Network\TrustedIp;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Constant;
@@ -478,7 +477,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
} else {
$log->setUser(new User('guest-' . hash('sha256', TrustedIp::extract($request, System::getEnv('_APP_TRUSTED_HEADERS', 'x-forwarded-for')))));
$log->setUser(new User('guest-' . hash('sha256', $request->getIP())));
}
$log->setNamespace("http");
+11 -35
View File
@@ -22,7 +22,6 @@ use Appwrite\Extend\Exception;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Cors;
use Appwrite\Network\Platform;
use Appwrite\Network\TrustedIp;
use Appwrite\Network\Validator\Origin;
use Appwrite\Network\Validator\Redirect;
use Appwrite\Utopia\Database\Documents\User;
@@ -162,14 +161,6 @@ App::setResource('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
/**
* List of domains served by the application.
*/
App::setResource('domains', fn () => array_unique(array_filter([
...\explode(',', System::getEnv('_APP_DOMAIN', 'localhost')),
...\explode(',', System::getEnv('_APP_CONSOLE_DOMAIN', 'localhost'))
])));
/**
* Platform configuration
*/
@@ -184,33 +175,16 @@ App::setResource('platform', function (Request $request) {
if ($request->getPort() === '80' && $protocol !== 'http') {
$port = ':80';
}
$platform['endpoint'] = "$protocol://{$platform['domain']}{$port}/v1";
$platform['endpoint'] = "$protocol://{$platform['apiHostname']}{$port}/v1";
return $platform;
}, ['request']);
/**
* Safe request origin used to construct urls
*/
App::setResource('origin', function (Request $request) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$port = '';
if ($request->getPort() === '443' && $protocol !== 'https') {
$port = ':443';
}
if ($request->getPort() === '80' && $protocol !== 'http') {
$port = ':80';
}
return "$protocol://{$request->getHostname()}{$port}";
}, ['request']);
/**
* List of allowed request hostnames for the request.
*/
App::setResource('allowedHostnames', function (array $domains, Document $project, Document $rule, Document $devKey, Request $request) {
$allowed = [...$domains];
App::setResource('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$allowed = [...($platform['hostnames'] ?? [])];
/* Add platform configured hostnames */
if (!$project->isEmpty() && $project->getId() !== 'console') {
@@ -224,14 +198,20 @@ App::setResource('allowedHostnames', function (array $domains, Document $project
$allowed[] = $request->getHostname();
}
/* Allow the request origin if a dev key or rule is found */
$originHostname = parse_url($request->getOrigin(), PHP_URL_HOST);
/* Add request hostname for preflight requests */
if ($request->getMethod() === 'OPTIONS') {
$allowed[] = $originHostname;
}
/* Allow the request origin if a dev key or rule is found */
if ((!$rule->isEmpty() || !$devKey->isEmpty()) && !empty($originHostname)) {
$allowed[] = $originHostname;
}
return array_unique($allowed);
}, ['domains', 'project', 'rule', 'devKey', 'request']);
}, ['platform', 'project', 'rule', 'devKey', 'request']);
/**
* List of allowed request schemes for the request.
@@ -860,10 +840,6 @@ App::setResource('servers', function () {
return $languages;
});
App::setResource('trustedIp', function (Request $request) {
return TrustedIp::extract($request, System::getEnv('_APP_TRUSTED_HEADERS', 'x-forwarded-for'));
}, ['request']);
App::setResource('promiseAdapter', function ($register) {
return $register->get('promiseAdapter');
}, ['register']);
+2 -2
View File
@@ -3,7 +3,7 @@
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Network\TrustedIp;
use Appwrite\Network\Validator\Origin;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
@@ -552,7 +552,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
*/
$timelimit = $timelimit('url:{url},ip:{ip}', 128, 60);
$timelimit
->setParam('{ip}', TrustedIp::extract($request, System::getEnv('_APP_TRUSTED_HEADERS', 'x-forwarded-for')))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getURI());
$abuse = new Abuse($timelimit);
+1 -1
View File
@@ -17,7 +17,7 @@ $buttons = [];
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$platform = Config::getParam('platform', []);
$hostname = $platform['consoleDomain'] ?? '';
$hostname = $platform['consoleHostname'] ?? '';
// TODO: remove this later
if (System::getEnv('_APP_ENV') === 'development') {
$hostname = 'localhost';
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

-48
View File
@@ -1,48 +0,0 @@
<?php
namespace Appwrite\Network;
use Appwrite\Utopia\Request;
class TrustedIp
{
/**
* Extract the trusted client IP address from a request.
*
* This method checks configured trusted headers (e.g., X-Forwarded-For)
* and falls back to the remote address if no valid IP is found.
*
* @param Request $request The Utopia request object
* @return string The trusted client IP address
*
*/
public static function extract(Request $request, string $trustedHeaders): string
{
// Fallback to remote address
$remoteAddr = $request->getServer('remote_addr') ?? '0.0.0.0';
$trustedHeaders = explode(',', $trustedHeaders);
$trustedHeaders = array_map('trim', $trustedHeaders);
$trustedHeaders = array_map('strtolower', $trustedHeaders);
$trustedHeaders = array_filter($trustedHeaders);
foreach ($trustedHeaders as $header) {
$headerValue = $request->getHeader($header);
if (empty($headerValue)) {
continue;
}
// Leftmost IP address is the address of the originating client
$ips = explode(',', $headerValue);
$ip = trim($ips[0]);
// Validate IP format (supports both IPv4 and IPv6)
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
return $remoteAddr;
}
}
@@ -59,7 +59,7 @@ class Get extends Action
->param('type', '', new WhiteList(['rules']), 'Resource type.')
->inject('response')
->inject('dbForPlatform')
->inject('domains')
->inject('platform')
->callback($this->action(...));
}
@@ -68,8 +68,9 @@ class Get extends Action
string $type,
Response $response,
Database $dbForPlatform,
array $domains
array $platform
) {
$domains = $platform['hostnames'] ?? [];
if ($type === 'rules') {
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
@@ -98,7 +98,6 @@ class Create extends Base
->inject('store')
->inject('proofForToken')
->inject('executor')
->inject('trustedIp')
->inject('platform')
->callback($this->action(...));
}
@@ -124,7 +123,6 @@ class Create extends Base
Store $store,
Token $proofForToken,
Executor $executor,
string $trustedIp,
array $platform
) {
$async = \strval($async) === 'true' || \strval($async) === '1';
@@ -240,7 +238,7 @@ class Create extends Base
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
$ip = $trustedIp;
$ip = $request->getIP();
$headers['x-appwrite-client-ip'] = $ip;
if (!empty($ip)) {
@@ -67,12 +67,13 @@ class Create extends Action
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('domains')
->inject('platform')
->callback($this->action(...));
}
public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, array $domains)
public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform)
{
$domains = $platform['hostnames'] ?? [];
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
@@ -72,12 +72,13 @@ class Create extends Action
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('domains')
->inject('platform')
->callback($this->action(...));
}
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $domains)
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform)
{
$domains = $platform['hostnames'] ?? [];
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
@@ -75,12 +75,13 @@ class Create extends Action
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('domains')
->inject('platform')
->callback($this->action(...));
}
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $domains)
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform)
{
$domains = $platform['hostnames'] ?? [];
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
@@ -72,12 +72,13 @@ class Create extends Action
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('domains')
->inject('platform')
->callback($this->action(...));
}
public function action(string $domain, string $siteId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $domains)
public function action(string $domain, string $siteId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform)
{
$domains = $platform['hostnames'] ?? [];
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
+59 -10
View File
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Tasks;
// Example usage: docker compose exec appwrite screenshot --templateId="playground-for-tanstack-start"
// Example of env vars flag: --variables="{\"VITE_FORMSPREE_FORM_ID\":\"xvgkbzll\", \"VITE_FORMSPREE_FORM_SECRET\":\"some_secret\"}"
// Expected output: public/images/sites/templates/playground-for-tanstack-start-light.png (and dark.png)
use Appwrite\ID;
@@ -10,6 +11,7 @@ use Tests\E2E\Client;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Platform\Action;
use Utopia\System\System;
use Utopia\Validator\Text;
class Screenshot extends Action
@@ -24,11 +26,24 @@ class Screenshot extends Action
$this
->desc('Create Site template screenshot')
->param('templateId', '', new Text(128), 'Template ID.')
->param('variables', '', new Text(16384), 'JSON of env variables to use when setting up the site.')
->callback($this->action(...));
}
public function action(string $templateId): void
public function action(string $templateId, string $variables): void
{
if (empty($variables)) {
$variables = [];
} else {
$variables = \json_decode($variables, true);
if (!\is_array($variables)) {
throw new \Exception('Invalid JSON in --variables flag');
}
}
if ($variables === null) {
throw new \Exception('Invalid JSON in --variables flag');
}
$templates = Config::getParam('templates-site', []);
$allowedTemplates = \array_filter($templates, function ($item) use ($templateId) {
@@ -133,6 +148,11 @@ class Screenshot extends Action
$framework = $template['frameworks'][0];
// Use best specifications to prevent out-of-memory during build
$specifications = Config::getParam('specifications', []);
$specifications = array_keys($specifications);
$specification = \end($specifications);
// Create site
$site = $client->call(Client::METHOD_POST, '/sites', [
'content-type' => 'application/json',
@@ -141,6 +161,7 @@ class Screenshot extends Action
'cookie' => $cookieConsole
], [
'siteId' => ID::unique(),
'specification' => $specification,
'name' => $template["name"],
'framework' => $framework['key'],
'adapter' => $framework['adapter'],
@@ -162,21 +183,48 @@ class Screenshot extends Action
$siteId = $site['body']['$id'];
// Prepare API key, incase it's needed as variable
$response = $client->call(Client::METHOD_POST, '/projects/' . $projectId . '/keys', [
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => $cookieConsole
], [
'name' => 'Screenshot API key',
'scopes' => \array_keys(Config::getParam('scopes', []))
]);
if ($response['headers']['status-code'] !== 201) {
Console::error(\json_encode($response));
throw new \Exception("Failed to create API key");
}
$apiKey = $response['body']['secret'];
Console::info("API key created");
$variables['APPWRITE_API_KEY'] = $apiKey;
// Create variables
if (!empty($template['variables'] ?? [])) {
foreach ($template['variables'] as $variable) {
if (empty($variable['value'] ?? '')) {
if (($variable['required'] ?? false) === true) {
throw new \Exception("Missing required variable: {$variable['name']}");
}
continue;
}
$name = $variable['name'];
$value = $variable['value'];
$value = \str_replace('{projectName}', $projectName, $value);
$value = \str_replace('{projectId}', $projectId, $value);
$value = \str_replace('{apiEndpoint}', 'http://localhost/v1', $value);
$value = \str_replace('{apiEndpoint}', 'http://' . System::getEnv('_APP_DOMAIN', '') . '/v1', $value);
if (\array_key_exists($name, $variables)) {
$value = $variables[$name];
}
if (empty($value)) {
if (($variable['required'] ?? false) === true) {
throw new \Exception("Missing required variable: {$variable['name']}. Provide it using the --variables flag to resolve this.");
}
continue;
}
$response = $client->call(Client::METHOD_POST, '/sites/' . $siteId . '/variables', [
'content-type' => 'application/json',
@@ -207,7 +255,8 @@ class Screenshot extends Action
'owner' => $template['providerOwner'],
'repository' => $template['providerRepositoryId'],
'rootDirectory' => $framework['providerRootDirectory'],
'version' => $template['providerVersion'],
'reference' => $template['providerVersion'],
'type' => 'tag',
'activate' => true,
]);
+2 -2
View File
@@ -119,7 +119,7 @@ class Comment
$i = 0;
foreach ($projects as $projectId => $project) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $this->platform['consoleDomain'] ?? '';
$hostname = $this->platform['consoleHostname'] ?? '';
$text .= "## {$project['name']}\n\n";
$text .= "Project ID: `{$projectId}`\n\n";
@@ -233,7 +233,7 @@ class Comment
public function generatImage(string $pathLight, string $pathDark, string $alt, int $width): string
{
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $this->platform['consoleDomain'] ?? '';
$hostname = $this->platform['consoleHostname'] ?? '';
$imageLight = $protocol . '://' . $hostname . $pathLight;
$imageDark = $protocol . '://' . $hostname . $pathDark;
+15
View File
@@ -184,7 +184,22 @@ class HTTPTest extends Scope
'origin' => 'http://google.com',
]);
$this->assertNull($response['headers']['access-control-allow-origin'] ?? null);
}
public function testPreflight()
{
$endpoint = '/v1/projects'; // Can be any non-404 route
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_OPTIONS, $endpoint, [
'origin' => 'http://random.com',
'access-control-request-headers' => 'X-Appwrite-Project',
'access-control-request-method' => 'GET'
]);
$this->assertEquals('http://random.com', $response['headers']['access-control-allow-origin']);
}
public function testConsoleRedirect()
@@ -326,38 +326,4 @@ trait AccountBase
$this->assertEquals($response['headers']['status-code'], 204);
}
public function testTrustedIp(): void
{
$email = uniqid() . 'user@localhost.test';
$password = 'password';
$name = 'User Name';
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-forwarded-for' => '203.0.113.195',
]), [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
]);
$this->assertEquals($response['headers']['status-code'], 201);
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-forwarded-for' => '203.0.113.195',
]), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals('203.0.113.195', $response['body']['clientIp'] ?? $response['body']['ip'] ?? '');
}
}