diff --git a/app/views/install/installer/css/styles.css b/app/views/install/installer/css/styles.css
index a5f3d126cd..c2623bf201 100644
--- a/app/views/install/installer/css/styles.css
+++ b/app/views/install/installer/css/styles.css
@@ -72,6 +72,7 @@
/* Overlay colors */
--overlay-neutral-hover: rgba(25, 25, 28, 0.03);
--overlay-neutral-pressed: rgba(25, 25, 28, 0.04);
+ --overlay-scrim: rgba(25, 25, 28, 0.8);
/* Icon sizes */
--icon-size-s: var(--base-16);
@@ -181,6 +182,235 @@
pointer-events: none;
}
+.installer-mock-dialog {
+ width: 100%;
+ padding: 0;
+ border: none;
+ background: none;
+ overflow: visible;
+}
+
+.installer-mock-dialog:focus {
+ outline: none;
+}
+
+.installer-mock-dialog::backdrop {
+ background: var(--overlay-scrim);
+ opacity: 0;
+ transition: opacity var(--duration-fast) var(--ease-in-out);
+}
+
+.installer-mock-panel {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden;
+ margin-inline: auto;
+ inline-size: 600px;
+ max-inline-size: 90vw;
+ border-radius: var(--border-radius-l);
+ border: var(--border-width-s) solid var(--border-neutral);
+ background: var(--bgcolor-neutral-primary);
+ color: var(--fgcolor-neutral-secondary);
+ box-shadow:
+ 0px 56px 32px 0px rgba(0, 0, 0, 0.02),
+ 0px 6px 14px 0px rgba(0, 0, 0, 0.04),
+ 0px 24px 25px 0px rgba(0, 0, 0, 0.03);
+ transform: scale(0.96);
+ opacity: 0;
+ transition:
+ transform var(--duration-medium) var(--ease-in-out),
+ opacity var(--duration-medium) var(--ease-in-out);
+}
+
+.installer-mock-header {
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--gap-xxxs);
+ padding: var(--space-8);
+ padding-block-start: var(--space-7);
+ border-bottom: var(--border-width-s) solid var(--border-neutral);
+ background: var(--bgcolor-neutral-primary);
+}
+
+.installer-mock-titlebar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--gap-xl);
+ width: 100%;
+}
+
+.mock-dialog-close {
+ width: 32px;
+ height: 32px;
+ border: var(--border-width-s) solid transparent;
+ background: transparent;
+ color: var(--fgcolor-neutral-tertiary);
+ border-radius: var(--border-radius-s);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: color var(--duration-fast) var(--ease-in-out), background var(--duration-fast) var(--ease-in-out);
+}
+
+.mock-dialog-close svg {
+ width: 20px;
+ height: 20px;
+}
+
+.mock-dialog-close:hover {
+ color: var(--fgcolor-neutral-secondary);
+ background: var(--overlay-neutral-hover);
+}
+
+.mock-dialog-close:active {
+ color: var(--fgcolor-neutral-secondary);
+ background: var(--overlay-neutral-pressed);
+}
+
+.mock-dialog-close:focus-visible {
+ outline: var(--border-width-l) solid var(--border-focus);
+ outline-offset: var(--border-width-s);
+}
+
+.installer-mock-content {
+ width: 100%;
+ padding: var(--space-8);
+ max-height: 70vh;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-8);
+}
+
+.mock-control {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-4);
+}
+
+.mock-control-text {
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap-none);
+}
+
+.mock-control-text p {
+ margin: 0;
+}
+
+.mock-control.is-disabled {
+ opacity: 0.5;
+}
+
+.mock-control.is-disabled .mock-switch {
+ cursor: not-allowed;
+}
+
+.mock-switch {
+ position: relative;
+ width: 36px;
+ height: 20px;
+ flex-shrink: 0;
+ cursor: pointer;
+}
+
+.mock-switch input {
+ position: absolute;
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.mock-switch-track {
+ position: absolute;
+ inset: 0;
+ border-radius: 999px;
+ background: var(--bgcolor-neutral-secondary);
+ border: 1px solid var(--border-neutral-strong);
+ transition: background var(--duration-medium) var(--ease-standard), border-color var(--duration-medium) var(--ease-standard);
+}
+
+.mock-switch-track::after {
+ content: '';
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ top: 1px;
+ left: 1px;
+ border-radius: 999px;
+ background: var(--bgcolor-neutral-primary);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12);
+ transition: transform var(--duration-medium) var(--ease-standard);
+}
+
+.mock-switch input:checked + .mock-switch-track {
+ background: var(--bgcolor-accent);
+ border-color: var(--bgcolor-accent);
+}
+
+.mock-switch input:checked + .mock-switch-track::after {
+ transform: translateX(16px);
+}
+
+.mock-switch input:focus-visible + .mock-switch-track {
+ box-shadow: 0 0 0 2px rgba(253, 54, 110, 0.3);
+}
+
+.mock-switch input:disabled + .mock-switch-track {
+ background: var(--bgcolor-neutral-tertiary);
+ border-color: var(--border-neutral);
+ cursor: not-allowed;
+}
+
+@media (max-width: 640px) {
+ .installer-mock-panel {
+ inline-size: 100%;
+ max-inline-size: calc(100% - 24px);
+ }
+
+ .installer-mock-header {
+ padding: var(--space-6);
+ }
+
+ .installer-mock-content {
+ padding: var(--space-6);
+ }
+
+ .mock-control {
+ align-items: flex-start;
+ }
+}
+
+.installer-mock-dialog[open]::backdrop {
+ opacity: 1;
+ animation: mock-backdrop-enter var(--duration-fast) var(--ease-in-out) forwards;
+}
+
+.installer-mock-dialog[open] .installer-mock-panel {
+ opacity: 1;
+ animation: mock-modal-show var(--duration-medium) var(--ease-in-out) forwards;
+}
+
+@keyframes mock-modal-show {
+ to {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+@keyframes mock-backdrop-enter {
+ to {
+ opacity: 1;
+ }
+}
+
.installer-toast {
inline-size: 24rem;
display: inline-flex;
@@ -284,6 +514,10 @@
outline-offset: var(--border-width-s);
}
+.is-hidden {
+ display: none !important;
+}
+
@media (min-width: 768px) {
.installer-toast-stack {
top: var(--base-24);
@@ -348,7 +582,7 @@ body {
z-index: 0;
}
-.installer-page[data-step='4'] .installer-backdrop {
+.installer-page[data-step='5'] .installer-backdrop {
opacity: 0.4;
}
@@ -391,7 +625,7 @@ body {
overflow: hidden;
}
-.installer-page[data-step='4'] .installer-card {
+.installer-page[data-step='5'] .installer-card {
opacity: 0;
pointer-events: none;
}
@@ -439,7 +673,7 @@ body {
z-index: 1;
}
-.installer-page[data-step='4'] .install-screen {
+.installer-page[data-step='5'] .install-screen {
opacity: 1;
pointer-events: auto;
}
@@ -834,6 +1068,8 @@ body {
border-radius: 0 0 var(--border-radius-m) var(--border-radius-m);
display: flex;
justify-content: flex-end;
+ flex-direction: row;
+ gap: var(--gap-m);
}
.install-error-details .button {
@@ -1346,10 +1582,17 @@ body {
gap: var(--gap-m);
width: 100%;
padding: var(--space-6);
- background: var(--bgcolor-warning-weaker);
+ background: var(--bgcolor-neutral-default);
border-radius: var(--border-radius-s);
- outline: var(--border-width-s) solid var(--border-warning-weak);
+ outline: var(--border-width-s) solid var(--border-neutral-strong);
outline-offset: -1px;
+ --alert-primary-color: var(--fgcolor-neutral-secondary);
+}
+
+.inline-alert--warning {
+ background: var(--bgcolor-warning-weaker);
+ outline-color: var(--border-warning-weak);
+ --alert-primary-color: var(--fgcolor-warning);
}
.inline-alert-content {
@@ -1365,7 +1608,7 @@ body {
display: inline-flex;
align-items: center;
justify-content: center;
- color: var(--fgcolor-warning);
+ color: var(--alert-primary-color);
flex-shrink: 0;
}
@@ -1488,6 +1731,7 @@ body {
background: transparent;
color: var(--fgcolor-neutral-secondary);
cursor: pointer;
+ border-radius: var(--border-radius-xs);
}
.input-icon-button svg {
@@ -1496,6 +1740,30 @@ body {
display: block;
}
+.input-icon-button:hover {
+ background: var(--overlay-neutral-hover);
+}
+
+.input-icon-button:active {
+ background: var(--overlay-neutral-pressed);
+}
+
+.password-toggle-icon {
+ display: inline-flex;
+}
+
+.password-toggle-icon.is-hidden {
+ display: none;
+}
+
+.password-toggle.is-visible [data-password-icon="show"] {
+ display: none;
+}
+
+.password-toggle.is-visible [data-password-icon="hide"] {
+ display: inline-flex;
+}
+
.icon-button {
display: inline-flex;
align-items: center;
diff --git a/app/views/install/installer/icons/eye-off.svg b/app/views/install/installer/icons/eye-off.svg
new file mode 100644
index 0000000000..9c0b0a063b
--- /dev/null
+++ b/app/views/install/installer/icons/eye-off.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/views/install/installer/icons/eye.svg b/app/views/install/installer/icons/eye.svg
new file mode 100644
index 0000000000..897b425bf3
--- /dev/null
+++ b/app/views/install/installer/icons/eye.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/views/install/installer/js/installer.js b/app/views/install/installer/js/installer.js
index 893bbf6e4e..463b7f6221 100644
--- a/app/views/install/installer/js/installer.js
+++ b/app/views/install/installer/js/installer.js
@@ -9,13 +9,11 @@
if (!stepContainer || !installerCard) return;
- const { isMockToastMode } = window.InstallerStepsContext || {};
const { validateInstallRequest } = window.InstallerStepsProgress || {};
- const { showToast } = window.InstallerToast || {};
const isUpgrade = document.body?.dataset.upgrade === 'true';
- const stepFlow = isUpgrade ? [1, 3, 4] : [1, 2, 3, 4];
- const cardSteps = stepFlow.filter((step) => step !== 4);
+ const stepFlow = isUpgrade ? [1, 4, 5] : [1, 2, 3, 4, 5];
+ const cardSteps = stepFlow.filter((step) => step !== 5);
const normalizeStep = (step) => {
const numeric = clampStep(step);
@@ -32,13 +30,13 @@
const buildStepConfig = () => {
const config = {};
stepFlow.forEach((step, index) => {
- if (step === 4) {
+ if (step === 5) {
config[step] = { back: { target: null }, next: { target: null } };
return;
}
const prev = stepFlow[index - 1] ?? null;
const next = stepFlow[index + 1] ?? null;
- const label = next === 4 ? (isUpgrade ? 'Update' : 'Install') : 'Next';
+ const label = next === 5 ? (isUpgrade ? 'Update' : 'Install') : 'Next';
config[step] = {
back: { target: prev },
next: { label, target: next }
@@ -55,7 +53,7 @@
let pendingStep = null;
let pendingPushState = false;
- const clampStep = (step) => Math.max(1, Math.min(4, step));
+ const clampStep = (step) => Math.max(1, Math.min(5, step));
const isInstallLocked = () => Boolean(window.InstallerSteps?.isInstallLocked?.());
const scrollToFirstError = (panel) => {
@@ -331,7 +329,7 @@
fetchStepHtml(targetStep, url)
.then((html) => {
- if (targetStep === 4) {
+ if (targetStep === 5) {
showInstallScreen(targetStep, html);
isTransitioning = false;
if (pendingStep !== null && pendingStep !== targetStep) {
@@ -370,8 +368,8 @@
const requestStep = (step, pushState) => {
const targetStep = normalizeStep(Number(step));
- if (isInstallLocked() && targetStep !== 4) {
- loadStep(4, true);
+ if (isInstallLocked() && targetStep !== 5) {
+ loadStep(5, true);
return;
}
if (isTransitioning) {
@@ -401,23 +399,14 @@
}
}
}
- if (action === 'next' && String(target) === '4' && typeof isMockToastMode === 'function' && isMockToastMode()) {
- showToast?.({
- status: 'error',
- title: 'Session expired',
- description: 'Refresh the page and try again.',
- dismissible: true
- });
- return;
- }
- if (action === 'next' && String(target) === '4' && typeof validateInstallRequest === 'function') {
+ if (action === 'next' && String(target) === '5' && typeof validateInstallRequest === 'function') {
const isValid = await validateInstallRequest();
if (!isValid) {
return;
}
}
- if (isInstallLocked() && Number(target) !== 4) {
- requestStep(4, true);
+ if (isInstallLocked() && Number(target) !== 5) {
+ requestStep(5, true);
return;
}
requestStep(target, true);
@@ -425,8 +414,8 @@
window.addEventListener('popstate', (event) => {
const step = event.state?.step || getStepFromUrl();
- if (isInstallLocked() && Number(step) !== 4) {
- requestStep(4, false);
+ if (isInstallLocked() && Number(step) !== 5) {
+ requestStep(5, false);
return;
}
requestStep(step, false);
@@ -434,10 +423,10 @@
document.addEventListener('DOMContentLoaded', () => {
let step = getStepFromUrl();
- if (isInstallLocked() && step !== 4) {
- const url = buildStepUrl(4);
- window.history.replaceState({ step: 4 }, '', url.toString());
- step = 4;
+ if (isInstallLocked() && step !== 5) {
+ const url = buildStepUrl(5);
+ window.history.replaceState({ step: 5 }, '', url.toString());
+ step = 5;
} else {
const url = buildStepUrl(step);
window.history.replaceState({ step }, '', url.toString());
@@ -445,7 +434,7 @@
const activePanel = stepContainer.querySelector('.step-panel') || stepContainer;
runStepInit(step, activePanel);
measureStepHeight(activePanel);
- if (step === 4 && installScreen) {
+ if (step === 5 && installScreen) {
runStepInit(step, installScreen);
}
const preload = () => {
diff --git a/app/views/install/installer/js/modules/context.js b/app/views/install/installer/js/modules/context.js
index 8e941cabe7..38bba74cbb 100644
--- a/app/views/install/installer/js/modules/context.js
+++ b/app/views/install/installer/js/modules/context.js
@@ -2,27 +2,13 @@
const getBodyDataset = () => document.body?.dataset ?? {};
const isUpgradeMode = () => getBodyDataset().upgrade === 'true';
const getLockedDatabase = () => getBodyDataset().lockedDatabase || '';
- const isMockMode = () => document.body?.dataset.installMode === 'mock'
- || (typeof navigator !== 'undefined' && navigator.webdriver);
- const isMockErrorMode = () => {
- if (typeof window === 'undefined') return false;
- const params = new URLSearchParams(window.location.search);
- return params.get('mock-error') === '1';
- };
- const getMockToastParam = () => {
- if (typeof window === 'undefined') return false;
- const params = new URLSearchParams(window.location.search);
- return params.get('mock-toast') === '1';
- };
- const MOCK_TOAST_ENABLED = getMockToastParam();
- const isMockToastMode = () => MOCK_TOAST_ENABLED;
- const isMockProgressMode = () => isMockMode() || isMockErrorMode();
const STEP_IDS = Object.freeze({
CONFIG_FILES: 'config-files',
DOCKER_COMPOSE: 'docker-compose',
ENV_VARS: 'env-vars',
- DOCKER_CONTAINERS: 'docker-containers'
+ DOCKER_CONTAINERS: 'docker-containers',
+ ACCOUNT_SETUP: 'account-setup'
});
const STATUS = Object.freeze({
@@ -80,6 +66,11 @@
id: STEP_IDS.DOCKER_CONTAINERS,
inProgress: 'Starting Docker containers...',
done: 'Docker containers started'
+ },
+ {
+ id: STEP_IDS.ACCOUNT_SETUP,
+ inProgress: 'Creating Appwrite account...',
+ done: 'Appwrite account created (redirecting...)'
}
]);
@@ -98,17 +89,13 @@
const clampStep = (step) => {
const numeric = Number(step);
if (Number.isNaN(numeric)) return 1;
- return Math.max(1, Math.min(4, numeric));
+ return Math.max(1, Math.min(5, numeric));
};
window.InstallerStepsContext = Object.freeze({
getBodyDataset,
isUpgradeMode,
getLockedDatabase,
- isMockMode,
- isMockErrorMode,
- isMockToastMode,
- isMockProgressMode,
STEP_IDS,
STATUS,
SSE_EVENTS,
diff --git a/app/views/install/installer/js/modules/mock-controls.js b/app/views/install/installer/js/modules/mock-controls.js
new file mode 100644
index 0000000000..1a887132fc
--- /dev/null
+++ b/app/views/install/installer/js/modules/mock-controls.js
@@ -0,0 +1,97 @@
+(() => {
+ const { isMockMode, MOCK_SETTINGS_KEY, readMockSettings } = window.InstallerMock || {};
+ if (!isMockMode?.()) return;
+
+ const dialog = document.querySelector('[data-mock-dialog]');
+ if (!dialog) return;
+
+ const footerLogo = document.querySelector('.installer-footer .appwrite-logo');
+ const closeButtons = dialog.querySelectorAll('[data-mock-close]');
+ const toggleInputs = Array.from(dialog.querySelectorAll('[data-mock-toggle]'));
+
+ const readSettings = () => readMockSettings?.() ?? {};
+
+ const writeSettings = (settings) => {
+ try {
+ sessionStorage.setItem(MOCK_SETTINGS_KEY, JSON.stringify(settings));
+ } catch (error) {}
+ };
+
+ const normalizeSettings = (settings) => ({
+ error: Boolean(settings?.error),
+ toast: Boolean(settings?.toast),
+ accountError: Boolean(settings?.accountError)
+ });
+
+ const applySettingsToUI = (settings) => {
+ const normalized = normalizeSettings(settings);
+ toggleInputs.forEach((input) => {
+ const key = input.dataset.mockToggle;
+ if (!key) return;
+ input.checked = Boolean(normalized[key]);
+ if (key === 'accountError') {
+ input.disabled = normalized.error;
+ input.closest('.mock-control')?.classList.toggle('is-disabled', normalized.error);
+ }
+ if (key === 'error') {
+ input.disabled = normalized.accountError;
+ input.closest('.mock-control')?.classList.toggle('is-disabled', normalized.accountError);
+ }
+ });
+ };
+
+ const updateSetting = (key, value) => {
+ const current = normalizeSettings(readSettings());
+ const next = { ...current, [key]: value };
+
+ // Make error and accountError mutually exclusive
+ if (key === 'error' && value) {
+ next.accountError = false;
+ }
+ if (key === 'accountError' && value) {
+ next.error = false;
+ }
+
+ writeSettings(next);
+ applySettingsToUI(next);
+ };
+
+ const openDialog = () => {
+ applySettingsToUI(readSettings());
+ if (typeof dialog.showModal === 'function') {
+ dialog.showModal();
+ } else {
+ dialog.setAttribute('open', '');
+ }
+ };
+
+ const closeDialog = () => {
+ if (dialog.open) {
+ dialog.close();
+ } else {
+ dialog.removeAttribute('open');
+ }
+ };
+
+ footerLogo?.addEventListener('dblclick', openDialog);
+ closeButtons.forEach((button) => button.addEventListener('click', closeDialog));
+ dialog.addEventListener('cancel', (event) => {
+ event.preventDefault();
+ closeDialog();
+ });
+ dialog.addEventListener('click', (event) => {
+ if (event.target === dialog) {
+ closeDialog();
+ }
+ });
+
+ toggleInputs.forEach((input) => {
+ input.addEventListener('change', (event) => {
+ const key = event.target?.dataset?.mockToggle;
+ if (!key) return;
+ updateSetting(key, event.target.checked);
+ });
+ });
+
+ applySettingsToUI(readSettings());
+})();
diff --git a/app/views/install/installer/js/modules/mock.js b/app/views/install/installer/js/modules/mock.js
new file mode 100644
index 0000000000..e1f1a883fe
--- /dev/null
+++ b/app/views/install/installer/js/modules/mock.js
@@ -0,0 +1,40 @@
+(() => {
+ const getBodyDataset = () => document.body?.dataset ?? {};
+ const isMockMode = () => getBodyDataset().installMode === 'mock'
+ || (typeof navigator !== 'undefined' && navigator.webdriver);
+
+ const MOCK_SETTINGS_KEY = 'appwrite-installer-mock-settings';
+
+ const readMockSettings = () => {
+ if (typeof window === 'undefined') return {};
+ try {
+ const raw = sessionStorage.getItem(MOCK_SETTINGS_KEY);
+ if (!raw) return {};
+ const parsed = JSON.parse(raw);
+ if (!parsed || typeof parsed !== 'object') return {};
+ return parsed;
+ } catch (error) {
+ return {};
+ }
+ };
+
+ const getMockFlag = (key) => {
+ const settings = readMockSettings();
+ return Boolean(settings?.[key]);
+ };
+
+ const isMockErrorMode = () => isMockMode() && getMockFlag('error');
+ const isMockToastMode = () => isMockMode() && getMockFlag('toast');
+ const isMockAccountErrorMode = () => isMockMode() && getMockFlag('accountError');
+ const isMockProgressMode = () => isMockMode() || isMockErrorMode();
+
+ window.InstallerMock = Object.freeze({
+ isMockMode,
+ isMockErrorMode,
+ isMockAccountErrorMode,
+ isMockToastMode,
+ isMockProgressMode,
+ MOCK_SETTINGS_KEY,
+ readMockSettings
+ });
+})();
diff --git a/app/views/install/installer/js/modules/progress.js b/app/views/install/installer/js/modules/progress.js
index 8c8acce04c..fae6e1106c 100644
--- a/app/views/install/installer/js/modules/progress.js
+++ b/app/views/install/installer/js/modules/progress.js
@@ -2,15 +2,18 @@
const {
INSTALLATION_STEPS,
TIMINGS,
- isMockMode,
- isMockErrorMode,
- isMockProgressMode,
- isMockToastMode,
getBodyDataset,
STEP_IDS,
STATUS,
SSE_EVENTS
} = window.InstallerStepsContext;
+ const {
+ isMockMode,
+ isMockErrorMode,
+ isMockAccountErrorMode,
+ isMockProgressMode,
+ isMockToastMode
+ } = window.InstallerMock || {};
const {
formState,
applyLockPayload,
@@ -49,11 +52,11 @@
};
const validateInstallRequest = async () => {
- if (isMockProgressMode?.()) return true;
if (isMockToastMode?.()) {
showCsrfToast();
return false;
}
+ if (isMockProgressMode?.()) return true;
try {
const response = await fetch('/install/validate', {
method: 'POST',
@@ -136,6 +139,13 @@
if (text) {
text.textContent = label;
}
+
+ // Show/hide "Navigate to Console" button for account setup errors
+ const consoleBtn = row.querySelector('[data-install-console]');
+ if (consoleBtn) {
+ const shouldShow = step.id === STEP_IDS.ACCOUNT_SETUP && status === STATUS.ERROR;
+ consoleBtn.classList.toggle('is-hidden', !shouldShow);
+ }
};
const normalizeInstallError = (message) => {
@@ -255,15 +265,28 @@
window.location.href = url;
};
- const notifyInstallComplete = (installId) => {
+ const notifyInstallComplete = (installId, session) => {
if (isMockProgressMode?.()) return Promise.resolve();
if (!installId) return Promise.resolve();
+ const payload = { installId };
+ const sessionSecret = session?.sessionSecret || session?.secret;
+ const sessionId = session?.sessionId || session?.id;
+ const sessionExpire = session?.sessionExpire || session?.expire;
+ if (sessionSecret) {
+ payload.sessionSecret = sessionSecret;
+ }
+ if (sessionId) {
+ payload.sessionId = sessionId;
+ }
+ if (sessionExpire) {
+ payload.sessionExpire = sessionExpire;
+ }
return fetch('/install/complete', {
method: 'POST',
headers: withCsrfHeader({
'Content-Type': 'application/json'
}),
- body: JSON.stringify({ installId })
+ body: JSON.stringify(payload)
}).catch(() => {});
};
@@ -277,6 +300,9 @@
const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443';
const normalizedEmail = (formState?.emailCertificates || '').trim();
const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim();
+ const normalizedAccountName = (formState?.accountName || '').trim();
+ const normalizedAccountEmail = (formState?.accountEmail || '').trim();
+ const normalizedAccountPassword = (formState?.accountPassword || '').trim();
return {
installId,
@@ -286,7 +312,10 @@
appDomain: normalizedDomain,
emailCertificates: normalizedEmail,
opensslKey: (formState?.opensslKey || '').trim(),
- assistantOpenAIKey: normalizedAssistantKey
+ assistantOpenAIKey: normalizedAssistantKey,
+ accountName: normalizedAccountName,
+ accountEmail: normalizedAccountEmail,
+ accountPassword: normalizedAccountPassword
};
};
@@ -358,7 +387,7 @@
}
};
- const initStep4 = (root) => {
+ const initStep5 = (root) => {
if (!root) return;
if (isMockProgressMode?.()) {
clearInstallLock?.();
@@ -697,7 +726,7 @@
const lastStep = INSTALLATION_STEPS[INSTALLATION_STEPS.length - 1];
if (lastStep) {
const lastState = progressState.get(lastStep.id);
- if (!lastState || lastState.status !== STATUS.COMPLETED) {
+ if (!lastState || (lastState.status !== STATUS.COMPLETED && lastState.status !== STATUS.ERROR)) {
progressState.set(lastStep.id, {
status: STATUS.COMPLETED,
message: lastStep.done,
@@ -706,9 +735,26 @@
renderProgress();
}
}
+ const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
+ const accountRequired = INSTALLATION_STEPS.some((step) => step.id === STEP_IDS.ACCOUNT_SETUP);
+ if (accountRequired && accountState?.status === STATUS.ERROR) {
+ finalizeInstall();
+ return;
+ }
+ const sessionDetails = accountState?.details;
+ if (accountRequired && !(sessionDetails?.sessionSecret || sessionDetails?.secret)) {
+ handleProgress({
+ step: STEP_IDS.ACCOUNT_SETUP,
+ status: STATUS.ERROR,
+ message: 'Account creation did not complete. Please retry.'
+ });
+ finalizeInstall();
+ return;
+ }
finalizeInstall();
- notifyInstallComplete(activeInstall?.installId);
- setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
+ notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
+ setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
+ });
return;
}
if (event === SSE_EVENTS.ERROR) {
@@ -808,24 +854,36 @@
};
list.addEventListener('click', (event) => {
- const button = event.target.closest('[data-install-retry]');
- if (!button) return;
- const row = button.closest('.install-row');
- const stepId = row?.dataset.step;
- retryInstallStep(stepId);
+ const consoleButton = event.target.closest('[data-install-console]');
+ const retryButton = event.target.closest('[data-install-retry]');
+
+ if (consoleButton) {
+ redirectToApp();
+ return;
+ }
+
+ if (retryButton) {
+ const row = retryButton.closest('.install-row');
+ const stepId = row?.dataset.step;
+ retryInstallStep(stepId);
+ }
});
const simulateInstallProgress = (options = {}) => {
if (activeInstall?.pollTimer) {
clearInterval(activeInstall.pollTimer);
}
- const startIndex = options.retryStep
+
+ let index = options.retryStep
? Math.max(0, INSTALLATION_STEPS.findIndex((step) => step.id === options.retryStep))
: 0;
- let index = startIndex;
- const errorStepId = isMockErrorMode?.() && !options.retryStep
- ? STEP_IDS.DOCKER_CONTAINERS
- : null;
+
+ let errorStepId = null;
+ if (isMockErrorMode?.() && !options.retryStep) {
+ errorStepId = isMockAccountErrorMode?.()
+ ? STEP_IDS.ACCOUNT_SETUP
+ : STEP_IDS.DOCKER_CONTAINERS;
+ }
const mockErrorDetails = (window.InstallerConstants || {}).mockErrorDetails || {
output: 'Error response from daemon: manifest for appwrite/appwrite:local not found: manifest unknown',
trace: '#0 /usr/src/code/src/Appwrite/Platform/Tasks/Install.php(540): Appwrite\\\\Platform\\\\Tasks\\\\Install->performInstallation(...)\\n#1 {main}'
@@ -858,10 +916,13 @@
if (errorStepId && step.id === errorStepId) {
index += 1;
activeInstall.pollTimer = setTimeout(() => {
+ const errorMessage = errorStepId === STEP_IDS.ACCOUNT_SETUP
+ ? 'A user with the same email already exists'
+ : 'Failed to start containers';
handleProgress({
step: step.id,
status: STATUS.ERROR,
- message: 'Failed to start containers',
+ message: errorMessage,
details: mockErrorDetails
});
finalizeInstall();
@@ -902,7 +963,7 @@
};
window.InstallerStepsProgress = {
- initStep4,
+ initStep5,
cleanupInstallFlow,
validateInstallRequest
};
diff --git a/app/views/install/installer/js/modules/state.js b/app/views/install/installer/js/modules/state.js
index c51b01aec2..74391f2b04 100644
--- a/app/views/install/installer/js/modules/state.js
+++ b/app/views/install/installer/js/modules/state.js
@@ -2,9 +2,9 @@
const {
getBodyDataset,
isUpgradeMode,
- getLockedDatabase,
- isMockProgressMode
+ getLockedDatabase
} = window.InstallerStepsContext || {};
+ const { isMockProgressMode } = window.InstallerMock || {};
const INSTALL_LOCK_KEY = 'appwrite-install-lock';
const INSTALL_ID_KEY = 'appwrite-install-id';
@@ -16,7 +16,10 @@
httpsPort: null,
emailCertificates: null,
opensslKey: null,
- assistantOpenAIKey: null
+ assistantOpenAIKey: null,
+ accountName: null,
+ accountEmail: null,
+ accountPassword: null
};
const dispatchStateChange = (key) => {
@@ -112,6 +115,9 @@
setStateIfEmpty('emailCertificates', payload.emailCertificates);
setStateIfEmpty('opensslKey', payload.opensslKey);
setStateIfEmpty('assistantOpenAIKey', payload.assistantOpenAIKey);
+ setStateIfEmpty('accountName', payload.accountName);
+ setStateIfEmpty('accountEmail', payload.accountEmail);
+ setStateIfEmpty('accountPassword', payload.accountPassword);
};
const getStoredInstallId = () => {
diff --git a/app/views/install/installer/js/modules/validation.js b/app/views/install/installer/js/modules/validation.js
index 78a6d7e20c..1f8b0ca9d3 100644
--- a/app/views/install/installer/js/modules/validation.js
+++ b/app/views/install/installer/js/modules/validation.js
@@ -11,6 +11,11 @@
return numeric >= 1 && numeric <= 65535;
};
+ const isValidPassword = (value) => {
+ if (!value) return false;
+ return value.trim().length >= 8;
+ };
+
const isValidIPv4 = (host) => {
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return false;
return host.split('.').every((part) => {
@@ -104,6 +109,7 @@
window.InstallerStepsValidation = {
isValidEmail,
isValidPort,
+ isValidPassword,
isValidHostnameInput,
extractHostname,
isLocalHost
diff --git a/app/views/install/installer/js/steps.js b/app/views/install/installer/js/steps.js
index 1677812596..057496f2e5 100644
--- a/app/views/install/installer/js/steps.js
+++ b/app/views/install/installer/js/steps.js
@@ -8,9 +8,9 @@
const {
INSTALLATION_STEPS,
- clampStep,
- isMockMode
+ clampStep
} = Context;
+ const { isMockMode } = window.InstallerMock || {};
const {
formState,
@@ -28,7 +28,8 @@
const {
isValidEmail,
isValidPort,
- isValidHostnameInput
+ isValidHostnameInput,
+ isValidPassword
} = Validation;
const {
@@ -260,7 +261,62 @@
}
};
+ const hydrateStep3State = (root) => {
+ State.setStateIfEmpty?.('accountName', root.querySelector('#account-name')?.value);
+ State.setStateIfEmpty?.('accountEmail', root.querySelector('#account-email')?.value);
+ State.setStateIfEmpty?.('accountPassword', root.querySelector('#account-password')?.value);
+ };
+
+ const applyStep3State = (root) => {
+ const name = root.querySelector('#account-name');
+ if (name && formState.accountName) name.value = formState.accountName;
+
+ const email = root.querySelector('#account-email');
+ if (email && formState.accountEmail) email.value = formState.accountEmail;
+
+ const password = root.querySelector('#account-password');
+ if (password && formState.accountPassword) password.value = formState.accountPassword;
+ };
+
const initStep3 = (root) => {
+ if (!root) return;
+ if (isMockMode?.()) {
+ clearInstallLock?.();
+ clearInstallId?.();
+ }
+ syncInstallLockFlag?.();
+ applyLockPayload?.();
+ applyBodyDefaults?.();
+ hydrateStep3State(root);
+ applyStep3State(root);
+
+ const name = root.querySelector('#account-name');
+ const email = root.querySelector('#account-email');
+ const password = root.querySelector('#account-password');
+ const passwordToggle = root.querySelector('[data-password-toggle="account-password"]');
+
+ bindInputToState(name, 'accountName');
+ bindInputToState(email, 'accountEmail');
+ bindInputToState(password, 'accountPassword');
+
+ bindErrorClear?.(name);
+ bindErrorClear?.(email);
+ bindErrorClear?.(password);
+
+ if (password && passwordToggle) {
+ passwordToggle.addEventListener('click', () => {
+ const isVisible = passwordToggle.classList.toggle('is-visible');
+ password.type = isVisible ? 'text' : 'password';
+ passwordToggle.setAttribute('aria-label', isVisible ? 'Hide password' : 'Show password');
+ });
+ }
+
+ if (isInstallLocked?.()) {
+ disableControls?.(root);
+ }
+ };
+
+ const initStep4 = (root) => {
if (!root) return;
if (isMockMode?.()) {
clearInstallLock?.();
@@ -285,24 +341,26 @@
const root = container.querySelector('.step-layout') || container;
const normalized = clampStep?.(step) ?? 1;
Tooltips?.cleanupTooltipPortals?.();
- if (normalized !== 3 && reviewListener) {
+ if (normalized !== 4 && reviewListener) {
document.removeEventListener('installer:state-change', reviewListener);
reviewListener = null;
}
- if (normalized !== 4) {
+ if (normalized !== 5) {
Progress.cleanupInstallFlow?.();
}
if (normalized === 1) initStep1(root);
if (normalized === 2) initStep2(root);
if (normalized === 3) initStep3(root);
- if (normalized === 4) Progress.initStep4?.(root);
+ if (normalized === 4) initStep4(root);
+ if (normalized === 5) Progress.initStep5?.(root);
};
window.InstallerSteps = {
initStep1,
initStep2,
initStep3,
- initStep4: Progress.initStep4,
+ initStep4,
+ initStep5: Progress.initStep5,
installationSteps: INSTALLATION_STEPS || [],
isInstallLocked,
getInstallLock,
@@ -368,6 +426,37 @@
}
}
+ if (normalized === 3) {
+ clearFieldErrors?.(root);
+ let valid = true;
+ const name = root?.querySelector('#account-name');
+ const email = root?.querySelector('#account-email');
+ const password = root?.querySelector('#account-password');
+
+ if (!name || !name.value.trim()) {
+ setFieldError?.(name, 'Please enter a name');
+ valid = false;
+ }
+
+ if (!email || !email.value.trim()) {
+ setFieldError?.(email, 'Please enter an email address');
+ valid = false;
+ } else if (!isValidEmail?.(email.value.trim())) {
+ setFieldError?.(email, 'Please enter a valid email address');
+ valid = false;
+ }
+
+ if (!password || !password.value.trim()) {
+ setFieldError?.(password, 'Please enter a password');
+ valid = false;
+ } else if (!isValidPassword?.(password.value.trim())) {
+ setFieldError?.(password, 'Password must be at least 8 characters');
+ valid = false;
+ }
+
+ return valid;
+ }
+
return true;
}
};
diff --git a/app/views/install/installer/templates/steps/step-2.phtml b/app/views/install/installer/templates/steps/step-2.phtml
index 29c58ab58b..17a8f15339 100644
--- a/app/views/install/installer/templates/steps/step-2.phtml
+++ b/app/views/install/installer/templates/steps/step-2.phtml
@@ -13,7 +13,7 @@ $secretKeyValue = htmlspecialchars((string) $defaultSecretKey, ENT_QUOTES, 'UTF-