fix(console): link the new console everywhere and drop the legacy /console scheme by HarshMN2345 · Pull Request #13689 · appwrite/appwrite · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
879f850
fix(console): register /console web redirects only for the legacy URL…
HarshMN2345 Sep 15, 2026
891ba49
fix(router): link function build errors to function deployments
HarshMN2345 Sep 15, 2026
d16e886
chore(console): describe when the /console web redirects are registered
HarshMN2345 Sep 15, 2026
d097daa
Merge branch 'main' into fix/console-redirects-scheme
HarshMN2345 Sep 15, 2026
9ed2e71
Merge remote-tracking branch 'origin/main' into fix/console-redirects…
HarshMN2345 Sep 15, 2026
7d1121d
feat(console): build console links from _APP_CONSOLE_URL
HarshMN2345 Sep 15, 2026
514c9e4
fix(vcs): send redirect-less installation callbacks to project settings
HarshMN2345 Sep 15, 2026
9fd34b7
fix(console): redirect legacy web paths to console routes
HarshMN2345 Sep 15, 2026
9800cc1
Merge remote-tracking branch 'origin/fix/console-redirects-scheme' in…
HarshMN2345 Sep 15, 2026
87e0f9c
fix(console): allow redirects to the console host when _APP_CONSOLE_U…
HarshMN2345 Sep 15, 2026
2423bb9
Merge branch 'main' into fix/console-redirects-scheme
HarshMN2345 Sep 15, 2026
7cc7063
fix(console): build worker console links from platform consoleUrl
HarshMN2345 Sep 15, 2026
db029c3
Merge branch 'main' into fix/console-redirects-scheme
HarshMN2345 Sep 15, 2026
4ed17e6
fix(console): send /recover without a reset token to the recovery form
HarshMN2345 Sep 15, 2026
8788b6b
fix(account): hand OAuth2 session params only to the console's own relay
HarshMN2345 Sep 15, 2026
264f030
chore(variables): introduce _APP_CONSOLE_URL in 2.2.1
HarshMN2345 Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
3 changes: 3 additions & 0 deletions app/config/platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
*/
return [
'apiHostname' => System::getEnv('_APP_DOMAIN', 'localhost'),
// Also serves the API (VCS OAuth callbacks, avatars, images); never point it at the console web origin
'consoleHostname' => System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', 'localhost')),
// Console web app origin for links; it can live apart from the API host above
'consoleUrl' => \rtrim(System::getEnv('_APP_CONSOLE_URL', (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https') . '://' . 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'),
Expand Down
8 changes: 4 additions & 4 deletions app/config/variables.php
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,10 @@
'filter' => ''
],
[
'name' => '_APP_CONSOLE_URL_SCHEME',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets check who added this variable, and confirm with them that its OK to remove it with this PR

'description' => 'Console URL scheme used when the backend generates links to the console (OAuth callbacks, emails, error page CTAs, VCS comments). Set to \'root\' for the new console served at the root path (appwrite/new), or \'legacy\' for the older console served under the /console path prefix. The default value is \'legacy\'.',
'introduction' => '2.0.0',
'default' => 'legacy',
'name' => '_APP_CONSOLE_URL',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we 100% certain we need new variable? Can we not reuse some existing one? if so, why not?

'description' => 'Origin of the Appwrite console web app, such as https://console.example.com, used when the backend generates links to the console (OAuth callbacks, emails, error page CTAs, VCS comments and commit statuses). Set it when the console is served on a different host than the API. When empty, links use _APP_CONSOLE_DOMAIN (or _APP_DOMAIN) over https, or over http when _APP_OPTIONS_FORCE_HTTPS is disabled.',
'introduction' => '2.2.1',
'default' => '',
'required' => false,
'question' => '',
'filter' => ''
Expand Down
48 changes: 21 additions & 27 deletions app/controllers/api/account.php

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throughout entire PR, we touched MANY flows. Lets QA them manually, to make sure there is no regression, nothing breaks.
If we are not sure what flow a diff is and how to reproduce it, that would be big warning flag. If we dont 100% understand where some change applies, that might be the exact diff we will want to focus our manual QA.

Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,8 @@
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;

$legacyConsolePaths = System::getEnv('_APP_CONSOLE_URL_SCHEME', 'legacy') !== 'root';

$oauthDefaultSuccess = $legacyConsolePaths ? '/console/auth/oauth2/success' : '/auth/oauth2/success';
$oauthDefaultFailure = $legacyConsolePaths ? '/console/auth/oauth2/failure' : '/auth/oauth2/failure';
$oauthDefaultSuccess = '/auth/oauth2/success';
$oauthDefaultFailure = '/auth/oauth2/failure';

$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Geo $geo, Event $queueForEvents, Bus $bus, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {

Expand Down Expand Up @@ -1378,11 +1376,9 @@
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}

$host = $platform['consoleHostname'] ?? '';
$redirectBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
$redirectBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$redirectBase = $platform['consoleUrl'] ?? '';
// Only the derived console URL follows the request port; an explicit one carries its own
if (empty(System::getEnv('_APP_CONSOLE_URL')) && $port !== ($protocol === 'https' ? '443' : '80')) {
$redirectBase .= ':' . $port;
}

Expand Down Expand Up @@ -1512,7 +1508,8 @@
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->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, Geo $geo, Database $dbForPlatform, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
->inject('platform')
->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, Geo $geo, Database $dbForPlatform, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, bool $domainVerification, ?string $cookieDomain, Authorization $authorization, array $platform) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
Expand Down Expand Up @@ -1580,12 +1577,15 @@
if ($devKey->isEmpty() && !empty($state['failure']) && !$redirectValidator->isValid($state['failure'])) {
throw new Exception(Exception::PROJECT_INVALID_FAILURE_URL);
}
// The default relays live on the console host; the same path on any other allowed host is a customer page
$consoleHostname = \parse_url($platform['consoleUrl'] ?? '', PHP_URL_HOST);

$failure = [];
if (!empty($state['failure'])) {
$failure = URLParser::parse($state['failure']);
}

$failureRedirect = (function (string $type, ?string $message = null, ?int $code = null, ?\Throwable $previous = null, array $params = []) use ($failure, $response, $project, $oauthDefaultFailure) {
$failureRedirect = (function (string $type, ?string $message = null, ?int $code = null, ?\Throwable $previous = null, array $params = []) use ($failure, $response, $project, $oauthDefaultFailure, $consoleHostname) {
$exception = new Exception($type, $message, $code, $previous, params: $params);
if (!empty($failure)) {
$query = URLParser::parseQuery($failure['query']);
Expand All @@ -1596,7 +1596,7 @@
]);
// Mirror success path: default OAuth failure relay needs project to deep-link
// back into the native app via appwrite-callback-{project}://
if (($failure['path'] ?? '') === $oauthDefaultFailure) {
if ($failure['host'] === $consoleHostname && $failure['path'] === $oauthDefaultFailure) {
$query['project'] = $project->getId();
}
$failure['query'] = URLParser::unparseQuery($query);
Expand Down Expand Up @@ -2179,7 +2179,7 @@
;

// TODO: Remove this deprecated workaround - support only token
if ($state['success']['path'] == $oauthDefaultSuccess) {
if ($state['success']['host'] === $consoleHostname && $state['success']['path'] === $oauthDefaultSuccess) {
$query['project'] = $project->getId();
$query['domain'] = $cookieDomain;
$query['key'] = $store->getKey();
Expand Down Expand Up @@ -2285,13 +2285,11 @@
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}

$host = $platform['consoleHostname'] ?? '';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$redirectBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
$redirectBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$redirectBase = $platform['consoleUrl'] ?? '';
// Only the derived console URL follows the request port; an explicit one carries its own
if (empty(System::getEnv('_APP_CONSOLE_URL')) && $port !== ($protocol === 'https' ? '443' : '80')) {
$redirectBase .= ':' . $port;
}

Expand Down Expand Up @@ -2358,7 +2356,7 @@
->inject('proofForPassword')
->inject('platform')
->inject('authorization')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) use ($legacyConsolePaths) {
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
Expand Down Expand Up @@ -2496,17 +2494,13 @@

if (empty($url)) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$host = $platform['consoleHostname'] ?? '';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
$callbackBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$callbackBase = $platform['consoleUrl'] ?? '';
// Only the derived console URL follows the request port; an explicit one carries its own
if (empty(System::getEnv('_APP_CONSOLE_URL')) && $port !== ($protocol === 'https' ? '443' : '80')) {
$callbackBase .= ':' . $port;
}
$url = $legacyConsolePaths
? "{$callbackBase}/console/auth/magic-url"
: "{$callbackBase}/auth/magic-url";
$url = "{$callbackBase}/auth/magic-url";
}

$url = Template::parseURL($url);
Expand Down
35 changes: 11 additions & 24 deletions app/controllers/general.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}

$errorView = __DIR__ . '/../views/general/error.phtml';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$url = $protocol . '://' . $platform['consoleHostname'];
$url = $platform['consoleUrl'] ?? '';
$platformHostnames = $platform['hostnames'] ?? [];

if ($rule->isEmpty()) {
Expand All @@ -126,7 +125,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if (\str_ends_with($host, $denyDomain)) {
$exception = new AppwriteException(AppwriteException::RULE_NOT_FOUND, 'This domain is not connected to any Appwrite resources. Visit domains tab under function/site settings to configure it.', view: $errorView);

$exception->addCTA('Start with this domain', System::getEnv('_APP_CONSOLE_URL_SCHEME', 'legacy') !== 'root' ? "{$url}/console" : $url);
$exception->addCTA('Start with this domain', $url);
throw $exception;
}
}
Expand Down Expand Up @@ -205,11 +204,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$resourceId = $rule->getAttribute('deploymentResourceId', '');
$type = ($resourceType === 'site') ? 'sites' : 'functions';
$exception = new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, view: $errorView);
$region = $project->getAttribute('region', 'default');
$ctaUrl = System::getEnv('_APP_CONSOLE_URL_SCHEME', 'legacy') !== 'root'
? "/console/project-{$region}-{$projectId}/{$type}/{$resourceType}-{$resourceId}"
: "/projects/{$projectId}/{$type}/{$resourceId}";
$exception->addCTA('View deployments', $url . $ctaUrl);
$exception->addCTA('View deployments', $url . "/projects/{$projectId}/{$type}/{$resourceId}");
throw $exception;
}

Expand Down Expand Up @@ -294,11 +289,10 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}

if (!$authorized) {
$url = $protocol . "://" . $platform['consoleHostname'];
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($url . (System::getEnv('_APP_CONSOLE_URL_SCHEME', 'legacy') !== 'root' ? '/console/auth/preview?' : '/auth/preview?')
->redirect($url . '/auth/preview?'
. \http_build_query([
'projectId' => $projectId,
'origin' => $protocol . '://' . $host,
Expand Down Expand Up @@ -355,14 +349,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$allowAnyStatus = !\is_null($apiKey) && $apiKey->isDeploymentStatusIgnored();
if (!$allowAnyStatus && $deployment->getAttribute('status') !== 'ready') {
$status = $deployment->getAttribute('status');
$region = $project->getAttribute('region', 'default');
$legacyConsolePaths = System::getEnv('_APP_CONSOLE_URL_SCHEME', 'legacy') !== 'root';
$siteUrl = $legacyConsolePaths
? "/console/project-{$region}-{$project->getId()}/sites/site-{$resource->getId()}"
: "/projects/{$project->getId()}/sites/{$resource->getId()}";
$deploymentUrl = $legacyConsolePaths
? "{$siteUrl}/deployments/deployment-{$deployment->getId()}"
: "{$siteUrl}/deployments/{$deployment->getId()}";
$collection = $type === 'function' ? 'functions' : 'sites';
$resourceUrl = "/projects/{$project->getId()}/{$collection}/{$resource->getId()}";
// Function deployments are listed on the function page itself
$deploymentsUrl = $type === 'function' ? $resourceUrl : "{$resourceUrl}/deployments";
$deploymentUrl = "{$resourceUrl}/deployments/{$deployment->getId()}";

switch ($status) {
case 'failed':
Expand All @@ -371,7 +362,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
break;
case 'canceled':
$exception = new AppwriteException(AppwriteException::BUILD_CANCELED, view: $errorView);
$exception->addCTA('View deployments', $url . $siteUrl . '/deployments');
$exception->addCTA('View deployments', $url . $deploymentsUrl);
break;
default:
$exception = new AppwriteException(AppwriteException::BUILD_NOT_READY, view: $errorView);
Expand All @@ -386,11 +377,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$permissions = $resource->getAttribute('execute');
if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) {
$exception = new AppwriteException(AppwriteException::FUNCTION_EXECUTE_PERMISSION_MISSING, view: $errorView);
$region = $project->getAttribute('region', 'default');
$ctaUrl = System::getEnv('_APP_CONSOLE_URL_SCHEME', 'legacy') !== 'root'
? "/console/project-{$region}-{$project->getId()}/functions/function-{$resource->getId()}/settings"
: "/projects/{$project->getId()}/functions/{$resource->getId()}/settings";
$exception->addCTA('View settings', $url . $ctaUrl);
$exception->addCTA('View settings', $url . "/projects/{$project->getId()}/functions/{$resource->getId()}/settings");
throw $exception;
}
}
Expand Down
6 changes: 6 additions & 0 deletions app/init/resources/request.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@
$context->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$allowed = [...($platform['hostnames'] ?? [])];

/* Add the console host, the default OAuth2 redirects land on it even when _APP_CONSOLE_URL points elsewhere */
$consoleHostname = \parse_url($platform['consoleUrl'] ?? '', PHP_URL_HOST);
if (! empty($consoleHostname)) {
$allowed[] = $consoleHostname;
}

/* Add platform configured hostnames */
if (! $project->isEmpty() && $project->getId() !== 'console') {
$platforms = $project->getAttribute('platforms', []);
Expand Down
15 changes: 11 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ services:
- _APP_NOTIFICATIONS_TRACKING_SECRET
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_CONSOLE_URL_SCHEME=root
- _APP_CONSOLE_URL
- _APP_CONSOLE_TRUSTED_PROJECTS
- _APP_DOMAIN_TARGET_CNAME
- _APP_DOMAIN_TARGET_AAAA
Expand Down Expand Up @@ -427,8 +427,8 @@ services:
- _APP_LOGGING_FORMAT
- _APP_QUEUE_NAME
- _APP_DATABASE_SHARED_TABLES
- _APP_CONSOLE_URL_SCHEME=root
- _APP_CONSOLE_DOMAIN
- _APP_CONSOLE_URL
- _APP_CONSOLE_TRUSTED_PROJECTS
- _APP_DOMAIN
- _APP_DOMAIN_TARGET_CNAME
Expand Down Expand Up @@ -634,7 +634,10 @@ services:
- _APP_LOGGING_FORMAT
- _APP_WEBHOOK_MAX_FAILED_ATTEMPTS
- _APP_DATABASE_SHARED_TABLES
- _APP_CONSOLE_URL_SCHEME=root
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_CONSOLE_URL
appwrite-worker-deletes:
profiles:
- separate
Expand Down Expand Up @@ -847,7 +850,7 @@ services:
- _APP_OPTIONS_ROUTER_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_CONSOLE_URL_SCHEME=root
- _APP_CONSOLE_URL
- _APP_CONSOLE_TRUSTED_PROJECTS
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
Expand Down Expand Up @@ -942,6 +945,10 @@ services:
- _APP_LOGGING_CONFIG
- _APP_LOGGING_FORMAT
- _APP_DATABASE_SHARED_TABLES
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_CONSOLE_URL
extra_hosts:
- host.docker.internal:host-gateway

Expand Down
7 changes: 1 addition & 6 deletions src/Appwrite/Deployment/GitAction.php
Loading
Loading