mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
add: account creation.
feat: mocking options.
This commit is contained in:
@@ -19,9 +19,9 @@ if ($installMode === 'mock' && $defaultEmailCertificates === '') {
|
||||
}
|
||||
|
||||
$step = isset($_GET['step']) ? (int) $_GET['step'] : 1;
|
||||
$step = max(1, min(4, $step));
|
||||
if ($isUpgrade && $step === 2) {
|
||||
$step = 3;
|
||||
$step = max(1, min(5, $step));
|
||||
if ($isUpgrade && ($step === 2 || $step === 3)) {
|
||||
$step = 4;
|
||||
}
|
||||
|
||||
$partialFile = __DIR__ . "/installer/templates/steps/step-{$step}.phtml";
|
||||
@@ -34,7 +34,7 @@ if (isset($_GET['partial'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$cardStep = min(3, $step);
|
||||
$cardStep = min(4, $step);
|
||||
$stepFile = __DIR__ . "/installer/templates/steps/step-{$cardStep}.phtml";
|
||||
if (!is_file($stepFile)) {
|
||||
$stepFile = __DIR__ . "/installer/templates/steps/step-1.phtml";
|
||||
@@ -43,6 +43,8 @@ $cssVersion = @filemtime(__DIR__ . '/installer/css/styles.css') ?: time();
|
||||
$tooltipsVersion = @filemtime(__DIR__ . '/installer/js/tooltips.js') ?: time();
|
||||
$constantsVersion = @filemtime(__DIR__ . '/installer/js/constants.js') ?: time();
|
||||
$stepsContextVersion = @filemtime(__DIR__ . '/installer/js/modules/context.js') ?: time();
|
||||
$mockModuleVersion = @filemtime(__DIR__ . '/installer/js/modules/mock.js') ?: time();
|
||||
$mockControlsVersion = @filemtime(__DIR__ . '/installer/js/modules/mock-controls.js') ?: time();
|
||||
$stepsStateVersion = @filemtime(__DIR__ . '/installer/js/modules/state.js') ?: time();
|
||||
$stepsValidationVersion = @filemtime(__DIR__ . '/installer/js/modules/validation.js') ?: time();
|
||||
$stepsUiVersion = @filemtime(__DIR__ . '/installer/js/modules/ui.js') ?: time();
|
||||
@@ -64,6 +66,10 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time()
|
||||
<script src="installer/js/constants.js?v=<?php echo $constantsVersion; ?>" defer></script>
|
||||
<!-- Load context first; other modules depend on InstallerStepsContext. -->
|
||||
<script src="installer/js/modules/context.js?v=<?php echo $stepsContextVersion; ?>" defer></script>
|
||||
<script src="installer/js/modules/mock.js?v=<?php echo $mockModuleVersion; ?>" defer></script>
|
||||
<?php if ($installMode === 'mock') { ?>
|
||||
<script src="installer/js/modules/mock-controls.js?v=<?php echo $mockControlsVersion; ?>" defer></script>
|
||||
<?php } ?>
|
||||
<script src="installer/js/modules/state.js?v=<?php echo $stepsStateVersion; ?>" defer></script>
|
||||
<script src="installer/js/modules/validation.js?v=<?php echo $stepsValidationVersion; ?>" defer></script>
|
||||
<script src="installer/js/modules/ui.js?v=<?php echo $stepsUiVersion; ?>" defer></script>
|
||||
@@ -127,6 +133,10 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time()
|
||||
<span class="indicator-active"><?php include __DIR__ . '/installer/icons/indicator-active.svg'; ?></span>
|
||||
<span class="indicator-inactive"><?php include __DIR__ . '/installer/icons/indicator-inactive.svg'; ?></span>
|
||||
</span>
|
||||
<span class="step-indicator" data-step="4">
|
||||
<span class="indicator-active"><?php include __DIR__ . '/installer/icons/indicator-active.svg'; ?></span>
|
||||
<span class="indicator-inactive"><?php include __DIR__ . '/installer/icons/indicator-inactive.svg'; ?></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button class="button primary" type="button" data-action="next">
|
||||
@@ -137,7 +147,7 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time()
|
||||
</div>
|
||||
<div class="install-screen" aria-live="polite">
|
||||
<div class="install-screen-content">
|
||||
<?php if ($step === 4) { include __DIR__ . '/installer/templates/steps/step-4.phtml'; } ?>
|
||||
<?php if ($step === 5) { include __DIR__ . '/installer/templates/steps/step-5.phtml'; } ?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -148,6 +158,56 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time()
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<?php if ($installMode === 'mock') { ?>
|
||||
<dialog class="installer-mock-dialog" data-mock-dialog aria-labelledby="mock-dialog-title" aria-describedby="mock-dialog-description">
|
||||
<section class="installer-mock-panel">
|
||||
<header class="installer-mock-header">
|
||||
<div class="installer-mock-titlebar">
|
||||
<p id="mock-dialog-title" class="typography-title-s text-neutral-primary">Mock settings</p>
|
||||
<button type="button" class="mock-dialog-close" data-mock-close aria-label="Close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M5.293 5.293a1 1 0 0 1 1.414 0L10 8.586l3.293-3.293a1 1 0 1 1 1.414 1.414L11.414 10l3.293 3.293a1 1 0 0 1-1.414 1.414L10 11.414l-3.293 3.293a1 1 0 0 1-1.414-1.414L8.586 10 5.293 6.707a1 1 0 0 1 0-1.414Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p id="mock-dialog-description" class="typography-text-m-400 text-neutral-secondary">Adjust mock responses for this session.</p>
|
||||
</header>
|
||||
<div class="installer-mock-content">
|
||||
<div class="mock-control">
|
||||
<div class="mock-control-text">
|
||||
<p class="typography-text-m-500 text-neutral-primary">Simulate session expiry</p>
|
||||
<p class="typography-text-xs-400 text-neutral-secondary">Show the toast error before install starts.</p>
|
||||
</div>
|
||||
<label class="mock-switch">
|
||||
<input type="checkbox" data-mock-toggle="toast" aria-label="Simulate session expiry">
|
||||
<span class="mock-switch-track" aria-hidden="true"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mock-control">
|
||||
<div class="mock-control-text">
|
||||
<p class="typography-text-m-500 text-neutral-primary">Simulate install error</p>
|
||||
<p class="typography-text-xs-400 text-neutral-secondary">Show a failed step and retry flow.</p>
|
||||
</div>
|
||||
<label class="mock-switch">
|
||||
<input type="checkbox" data-mock-toggle="error" aria-label="Simulate install error">
|
||||
<span class="mock-switch-track" aria-hidden="true"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mock-control">
|
||||
<div class="mock-control-text">
|
||||
<p class="typography-text-m-500 text-neutral-primary">Simulate account error</p>
|
||||
<p class="typography-text-xs-400 text-neutral-secondary">Fail the account creation step.</p>
|
||||
</div>
|
||||
<label class="mock-switch">
|
||||
<input type="checkbox" data-mock-toggle="accountError" aria-label="Simulate account error">
|
||||
<span class="mock-switch-track" aria-hidden="true"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</dialog>
|
||||
<?php } ?>
|
||||
|
||||
<template id="field-error-template">
|
||||
<div class="field-error typography-text-xs-400 text-error">
|
||||
<span class="field-error-text"></span>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M3.707 2.293a1 1 0 1 0-1.414 1.414l14 14a1 1 0 1 0 1.414-1.414l-1.473-1.473A10 10 0 0 0 19.543 10C18.268 5.943 14.478 3 10 3a9.96 9.96 0 0 0-4.512 1.074zm4.261 4.26 1.514 1.515a2.003 2.003 0 0 1 2.45 2.45l1.515 1.514a4 4 0 0 0-5.478-5.478" clip-rule="evenodd"/>
|
||||
<path fill="currentColor" d="M12.454 16.697 9.75 13.992a4 4 0 0 1-3.742-3.742L2.335 6.578A10 10 0 0 0 .458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.67-.105 2.454-.303"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 568 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path fill="currentColor" d="M10 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4"/>
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M.457 10C1.732 5.943 5.522 3 10 3s8.267 2.943 9.542 7c-1.275 4.057-5.065 7-9.542 7-4.478 0-8.268-2.943-9.543-7M14 10a4 4 0 1 1-8 0 4 4 0 0 1 8 0" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 372 B |
@@ -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 = () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
})();
|
||||
@@ -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
|
||||
});
|
||||
})();
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ $secretKeyValue = htmlspecialchars((string) $defaultSecretKey, ENT_QUOTES, 'UTF-
|
||||
</div>
|
||||
|
||||
<div class="stack-xl">
|
||||
<div class="inline-alert">
|
||||
<div class="inline-alert inline-alert--warning">
|
||||
<div class="inline-alert-content">
|
||||
<div class="inline-alert-icon" aria-hidden="true">
|
||||
<?php include __DIR__ . '/../../icons/warning.svg'; ?>
|
||||
|
||||
@@ -1,72 +1,69 @@
|
||||
<?php
|
||||
$isUpgrade = $isUpgrade ?? false;
|
||||
$lockedDatabase = $lockedDatabase ?? null;
|
||||
$defaultAppDomain = $defaultAppDomain ?? 'localhost';
|
||||
$defaultHttpPort = $defaultHttpPort ?? '80';
|
||||
$defaultHttpsPort = $defaultHttpsPort ?? '443';
|
||||
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
|
||||
$defaultSecretKey = $defaultSecretKey ?? '';
|
||||
$defaultDatabase = $defaultDatabase ?? 'mongodb';
|
||||
$selectedDatabase = $lockedDatabase ?: $defaultDatabase;
|
||||
$defaultDatabaseLabel = $selectedDatabase === 'mariadb' ? 'MariaDB' : 'MongoDB';
|
||||
$badgeLabel = $defaultSecretKey !== '' ? 'Generated' : 'Missing';
|
||||
$badgeClass = $defaultSecretKey !== '' ? 'badge-success' : 'badge-warning';
|
||||
$defaultAccountEmail = $defaultAccountEmail ?? '';
|
||||
$defaultAccountName = $defaultAccountName ?? '';
|
||||
$accountEmailValue = htmlspecialchars($defaultAccountEmail, ENT_QUOTES, 'UTF-8');
|
||||
$accountNameValue = htmlspecialchars($defaultAccountName, ENT_QUOTES, 'UTF-8');
|
||||
?>
|
||||
<div class="step-layout" data-step="3">
|
||||
<div class="stack-xl">
|
||||
<div class="stack-xxxs">
|
||||
<h1 class="typography-title-s text-neutral-primary"><?php echo $isUpgrade ? 'Review your update' : 'Review your setup'; ?></h1>
|
||||
<h1 class="typography-title-s text-neutral-primary">Create your account</h1>
|
||||
<p class="typography-text-m-400 text-neutral-secondary">
|
||||
<?php echo $isUpgrade ? 'Confirm your settings before updating Appwrite.' : 'Check your settings below before completing the installation.'; ?>
|
||||
Set up the email and password for your Appwrite account. You can use these
|
||||
credentials to sign in later.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="stack-xl">
|
||||
<div class="review-card">
|
||||
<div class="stack-s">
|
||||
<div class="review-row">
|
||||
<div class="typography-text-m-500 text-neutral-primary" data-review-value="appDomain">
|
||||
<?php echo htmlspecialchars((string) $defaultAppDomain, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Hostname</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<div class="typography-text-m-500 text-neutral-primary" data-review-value="database">
|
||||
<?php echo htmlspecialchars((string) $defaultDatabaseLabel, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Database</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<div class="typography-text-m-500 text-neutral-primary" data-review-value="httpPort">
|
||||
<?php echo htmlspecialchars((string) $defaultHttpPort, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">HTTP port</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<div class="typography-text-m-500 text-neutral-primary" data-review-value="httpsPort">
|
||||
<?php echo htmlspecialchars((string) $defaultHttpsPort, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">HTTPS port</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<div class="typography-text-m-500 text-neutral-primary" data-review-value="emailCertificates">
|
||||
<?php echo htmlspecialchars((string) $defaultEmailCertificates, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">SSL certificate email</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<span class="badge badge-neutral typography-text-xs-400" data-review-assistant-badge>Disabled</span>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Appwrite Assistant</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<span class="badge <?php echo $badgeClass; ?> typography-text-xs-400" data-review-badge>
|
||||
<?php echo htmlspecialchars((string) $badgeLabel, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</span>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Secret API key</div>
|
||||
<div class="stack-l">
|
||||
<div class="input-group stack-xs">
|
||||
<label for="account-name" class="label-text typography-text-m-500 text-neutral-secondary">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="account-name"
|
||||
name="accountName"
|
||||
class="input-field typography-text-m-400 text-neutral-primary"
|
||||
placeholder="John Doe"
|
||||
value="<?php echo $accountNameValue; ?>"
|
||||
autocomplete="name"
|
||||
>
|
||||
</div>
|
||||
<div class="input-group stack-xs">
|
||||
<label for="account-email" class="label-text typography-text-m-500 text-neutral-secondary">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="account-email"
|
||||
name="accountEmail"
|
||||
class="input-field typography-text-m-400 text-neutral-primary"
|
||||
placeholder="you@example.com"
|
||||
value="<?php echo $accountEmailValue; ?>"
|
||||
autocomplete="email"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="input-group stack-xs">
|
||||
<label for="account-password"
|
||||
class="label-text typography-text-m-500 text-neutral-secondary">Password</label>
|
||||
<div class="input-action">
|
||||
<input
|
||||
type="password"
|
||||
id="account-password"
|
||||
name="accountPassword"
|
||||
class="input-action-input typography-text-m-400 text-neutral-primary"
|
||||
placeholder="Password must be at least 8 characters long"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<div class="input-action-buttons">
|
||||
<button type="button" class="input-icon-button password-toggle" aria-label="Show password" data-password-toggle="account-password">
|
||||
<span class="password-toggle-icon is-hidden" data-password-icon="hide">
|
||||
<?php include __DIR__ . '/../../icons/eye-off.svg'; ?>
|
||||
</span>
|
||||
<span class="password-toggle-icon" data-password-icon="show">
|
||||
<?php include __DIR__ . '/../../icons/eye.svg'; ?>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,49 +1,72 @@
|
||||
<?php
|
||||
$isUpgrade = $isUpgrade ?? false;
|
||||
$lockedDatabase = $lockedDatabase ?? null;
|
||||
$defaultAppDomain = $defaultAppDomain ?? 'localhost';
|
||||
$defaultHttpPort = $defaultHttpPort ?? '80';
|
||||
$defaultHttpsPort = $defaultHttpsPort ?? '443';
|
||||
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
|
||||
$defaultSecretKey = $defaultSecretKey ?? '';
|
||||
$defaultDatabase = $defaultDatabase ?? 'mongodb';
|
||||
$selectedDatabase = $lockedDatabase ?: $defaultDatabase;
|
||||
$defaultDatabaseLabel = $selectedDatabase === 'mariadb' ? 'MariaDB' : 'MongoDB';
|
||||
$badgeLabel = $defaultSecretKey !== '' ? 'Generated' : 'Missing';
|
||||
$badgeClass = $defaultSecretKey !== '' ? 'badge-success' : 'badge-warning';
|
||||
?>
|
||||
<div class="step-layout install-layout" data-step="4">
|
||||
<div class="install-card">
|
||||
<div class="install-panel">
|
||||
<div class="install-header">
|
||||
<div class="typography-text-m-400 text-neutral-primary">
|
||||
<?php echo $isUpgrade ? 'Updating your app…' : 'Installing your app…'; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-list" data-install-list></div>
|
||||
<div class="step-layout" data-step="4">
|
||||
<div class="stack-xl">
|
||||
<div class="stack-xxxs">
|
||||
<h1 class="typography-title-s text-neutral-primary"><?php echo $isUpgrade ? 'Review your update' : 'Review your setup'; ?></h1>
|
||||
<p class="typography-text-m-400 text-neutral-secondary">
|
||||
<?php echo $isUpgrade ? 'Confirm your settings before updating Appwrite.' : 'Check your settings below before completing the installation.'; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="install-row-template">
|
||||
<div class="install-row" data-status="in-progress">
|
||||
<div class="install-row-main">
|
||||
<div class="install-row-label">
|
||||
<span class="install-icon" aria-hidden="true">
|
||||
<span class="install-icon-spinner">
|
||||
<?php include __DIR__ . '/../../icons/install-spinner.svg'; ?>
|
||||
<div class="stack-xl">
|
||||
<div class="review-card">
|
||||
<div class="stack-s">
|
||||
<div class="review-row">
|
||||
<div class="typography-text-m-500 text-neutral-primary" data-review-value="appDomain">
|
||||
<?php echo htmlspecialchars((string) $defaultAppDomain, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Hostname</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<div class="typography-text-m-500 text-neutral-primary" data-review-value="database">
|
||||
<?php echo htmlspecialchars((string) $defaultDatabaseLabel, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Database</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<div class="typography-text-m-500 text-neutral-primary" data-review-value="httpPort">
|
||||
<?php echo htmlspecialchars((string) $defaultHttpPort, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">HTTP port</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<div class="typography-text-m-500 text-neutral-primary" data-review-value="httpsPort">
|
||||
<?php echo htmlspecialchars((string) $defaultHttpsPort, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">HTTPS port</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<div class="typography-text-m-500 text-neutral-primary" data-review-value="emailCertificates">
|
||||
<?php echo htmlspecialchars((string) $defaultEmailCertificates, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</div>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">SSL certificate email</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<span class="badge badge-neutral typography-text-xs-400" data-review-assistant-badge>Disabled</span>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Appwrite Assistant</div>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<span class="badge <?php echo $badgeClass; ?> typography-text-xs-400" data-review-badge>
|
||||
<?php echo htmlspecialchars((string) $badgeLabel, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</span>
|
||||
<span class="install-icon-check">
|
||||
<?php include __DIR__ . '/../../icons/install-check.svg'; ?>
|
||||
</span>
|
||||
<span class="install-icon-error">
|
||||
<?php include __DIR__ . '/../../icons/exclamation-circle.svg'; ?>
|
||||
</span>
|
||||
</span>
|
||||
<span class="install-text typography-text-m-400 text-neutral-primary" data-install-text></span>
|
||||
</div>
|
||||
<button type="button" class="install-row-toggle" aria-expanded="false" data-install-toggle>
|
||||
<?php include __DIR__ . '/../../icons/chevron-down.svg'; ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="install-row-details">
|
||||
<div class="install-row-details-inner">
|
||||
<pre class="install-error-code typography-text-xs-400" data-install-trace></pre>
|
||||
<div class="install-error-actions">
|
||||
<button type="button" class="button secondary" data-install-retry>
|
||||
<span class="button-text typography-text-m-500">Retry</span>
|
||||
</button>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Secret API key</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
$isUpgrade = $isUpgrade ?? false;
|
||||
?>
|
||||
<div class="step-layout install-layout" data-step="5">
|
||||
<div class="install-card">
|
||||
<div class="install-panel">
|
||||
<div class="install-header">
|
||||
<div class="typography-text-m-400 text-neutral-primary">
|
||||
<?php echo $isUpgrade ? 'Updating your app…' : 'Installing your app…'; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-list" data-install-list></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="install-row-template">
|
||||
<div class="install-row" data-status="in-progress">
|
||||
<div class="install-row-main">
|
||||
<div class="install-row-label">
|
||||
<span class="install-icon" aria-hidden="true">
|
||||
<span class="install-icon-spinner">
|
||||
<?php include __DIR__ . '/../../icons/install-spinner.svg'; ?>
|
||||
</span>
|
||||
<span class="install-icon-check">
|
||||
<?php include __DIR__ . '/../../icons/install-check.svg'; ?>
|
||||
</span>
|
||||
<span class="install-icon-error">
|
||||
<?php include __DIR__ . '/../../icons/exclamation-circle.svg'; ?>
|
||||
</span>
|
||||
</span>
|
||||
<span class="install-text typography-text-m-400 text-neutral-primary" data-install-text></span>
|
||||
</div>
|
||||
<button type="button" class="install-row-toggle" aria-expanded="false" data-install-toggle>
|
||||
<?php include __DIR__ . '/../../icons/chevron-down.svg'; ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="install-row-details">
|
||||
<div class="install-row-details-inner">
|
||||
<pre class="install-error-code typography-text-xs-400" data-install-trace></pre>
|
||||
<div class="install-error-actions">
|
||||
<button type="button" class="button secondary" data-install-retry>
|
||||
<span class="button-text typography-text-m-500">Retry</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="button primary is-hidden" data-install-console>
|
||||
<span class="button-text typography-text-m-500">Navigate to Console</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -172,11 +172,41 @@ class HttpHandler
|
||||
$input = [];
|
||||
}
|
||||
$installId = $this->state->sanitizeInstallId($input['installId'] ?? '');
|
||||
$sessionId = is_string($input['sessionId'] ?? null) ? $input['sessionId'] : null;
|
||||
$sessionSecret = is_string($input['sessionSecret'] ?? null) ? $input['sessionSecret'] : null;
|
||||
$sessionExpire = is_string($input['sessionExpire'] ?? null) ? $input['sessionExpire'] : null;
|
||||
|
||||
if ($installId !== '') {
|
||||
$this->state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
|
||||
}
|
||||
|
||||
if ($sessionSecret) {
|
||||
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||||
|| (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
|
||||
$sameSite = $isHttps ? 'None' : 'Lax';
|
||||
$expires = 0;
|
||||
if ($sessionExpire) {
|
||||
$timestamp = strtotime($sessionExpire);
|
||||
if ($timestamp !== false) {
|
||||
$expires = $timestamp;
|
||||
}
|
||||
}
|
||||
$cookieOptions = [
|
||||
'expires' => $expires,
|
||||
'path' => '/',
|
||||
'secure' => $isHttps,
|
||||
'httponly' => true,
|
||||
'samesite' => $sameSite,
|
||||
];
|
||||
setcookie('a_session_console', $sessionSecret, $cookieOptions);
|
||||
setcookie('a_session_console_legacy', $sessionSecret, $cookieOptions);
|
||||
if ($sessionId) {
|
||||
header('X-Appwrite-Session: ' . $sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@unlink(Server::INSTALLER_CONFIG_FILE);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
if (function_exists('fastcgi_finish_request')) {
|
||||
@@ -405,13 +435,19 @@ class HttpHandler
|
||||
}
|
||||
|
||||
$vars = $this->config->getVars();
|
||||
$envVars = $installer->prepareEnvironmentVariables($payloadInput, $vars);
|
||||
$shouldGenerateSecrets = !$installer->hasExistingConfig() && !$this->config->isUpgrade();
|
||||
$envVars = $installer->prepareEnvironmentVariables($payloadInput, $vars, $shouldGenerateSecrets);
|
||||
$account = [
|
||||
'name' => $input['accountName'] ?? '',
|
||||
'email' => $input['accountEmail'] ?? '',
|
||||
'password' => $input['accountPassword'] ?? '',
|
||||
];
|
||||
|
||||
$this->state->writeProgressFile($installId, [
|
||||
'payload' => [
|
||||
'httpPort' => $input['httpPort'] ?? $this->config->getDefaultHttpPort(),
|
||||
'httpsPort' => $input['httpsPort'] ?? $this->config->getDefaultHttpsPort(),
|
||||
'database' => $input['database'] ?? 'mongodb',
|
||||
'database' => $lockedDatabase ?? ($input['database'] ?? 'mongodb'),
|
||||
'appDomain' => $input['appDomain'] ?? 'localhost',
|
||||
'emailCertificates' => $input['emailCertificates'] ?? '',
|
||||
'opensslKeyHash' => $this->state->hashSensitiveValue((string) ($input['opensslKey'] ?? '')),
|
||||
@@ -450,7 +486,8 @@ class HttpHandler
|
||||
$this->config->getNoStart(),
|
||||
$progress,
|
||||
$retryStep,
|
||||
$this->config->isUpgrade()
|
||||
$this->config->isUpgrade(),
|
||||
$account
|
||||
);
|
||||
|
||||
if ($wantsStream) {
|
||||
@@ -487,6 +524,7 @@ class HttpHandler
|
||||
if ($installId !== '') {
|
||||
$this->state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
}
|
||||
@unlink(Server::INSTALLER_CONFIG_FILE);
|
||||
if ($wantsStream) {
|
||||
$details = ['trace' => $e->getTraceAsString()];
|
||||
$previous = $e->getPrevious();
|
||||
|
||||
@@ -329,7 +329,7 @@ class State
|
||||
|
||||
private function globalLockPath(): string
|
||||
{
|
||||
return sys_get_temp_dir() . '/appwrite-install-lock.json';
|
||||
return Server::INSTALLER_LOCK_FILE;
|
||||
}
|
||||
|
||||
private function isGlobalLockActive(?array $lock): bool
|
||||
|
||||
@@ -12,19 +12,23 @@ class Server
|
||||
{
|
||||
public const int INSTALLER_WEB_PORT = 20080;
|
||||
public const int INSTALLER_WEB_PORT_INTERNAL = 20080;
|
||||
|
||||
// temp files for state and config management!
|
||||
public const string INSTALLER_LOCK_FILE = '/tmp/appwrite-install-lock.json';
|
||||
public const string INSTALLER_CONFIG_FILE = '/tmp/appwrite-installer-config.json';
|
||||
|
||||
public const string STEP_ENV_VARS = 'env-vars';
|
||||
public const string STEP_CONFIG_FILES = 'config-files';
|
||||
public const string STEP_DOCKER_COMPOSE = 'docker-compose';
|
||||
public const string STEP_DOCKER_CONTAINERS = 'docker-containers';
|
||||
public const string STEP_ACCOUNT_SETUP = 'account-setup';
|
||||
|
||||
public const string STATUS_IN_PROGRESS = 'in-progress';
|
||||
public const string STATUS_COMPLETED = 'completed';
|
||||
public const string STATUS_ERROR = 'error';
|
||||
|
||||
private const string DEFAULT_IMAGE = 'appwrite-dev';
|
||||
private const string DEFAULT_CONTAINER = 'appwrite-installer';
|
||||
public const string DEFAULT_CONTAINER = 'appwrite-installer';
|
||||
private const string DEV_SERVER_START_PATTERN = '/PHP\s+\d+\.\d+\.\d+\s+Development Server .* started/';
|
||||
|
||||
private State $state;
|
||||
@@ -218,7 +222,8 @@ class Server
|
||||
}
|
||||
|
||||
$tempDir = sys_get_temp_dir();
|
||||
@unlink($tempDir . '/appwrite-install-lock.json');
|
||||
@unlink(self::INSTALLER_LOCK_FILE);
|
||||
@unlink(self::INSTALLER_CONFIG_FILE);
|
||||
foreach (glob($tempDir . '/appwrite-install-*.json') ?: [] as $file) {
|
||||
@unlink($file);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use Utopia\Auth\Proofs\Password;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Fetch\Client;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
@@ -19,6 +20,8 @@ class Install extends Action
|
||||
private const int INSTALL_STEP_DELAY_SECONDS = 2;
|
||||
private const int WEB_SERVER_CHECK_ATTEMPTS = 10;
|
||||
private const int WEB_SERVER_CHECK_DELAY_SECONDS = 1;
|
||||
private const int HEALTH_CHECK_ATTEMPTS = 10;
|
||||
private const int HEALTH_CHECK_DELAY_SECONDS = 3;
|
||||
|
||||
protected string $hostPath = '';
|
||||
protected ?bool $isLocalInstall = null;
|
||||
@@ -72,7 +75,8 @@ class Install extends Action
|
||||
|
||||
// Check for existing installation
|
||||
$data = $this->readExistingCompose();
|
||||
$existingInstallation = $data !== '';
|
||||
$envFileExists = file_exists($this->path . '/' . $this->getEnvFileName());
|
||||
$existingInstallation = $data !== '' || $envFileExists;
|
||||
|
||||
if ($existingInstallation) {
|
||||
$time = \time();
|
||||
@@ -236,7 +240,8 @@ class Install extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$input = $this->prepareEnvironmentVariables($userInput, $vars);
|
||||
$shouldGenerateSecrets = !$existingInstallation && !$isUpgrade;
|
||||
$input = $this->prepareEnvironmentVariables($userInput, $vars, $shouldGenerateSecrets);
|
||||
$this->performInstallation($httpPort, $httpsPort, $organization, $image, $input, $noStart, null, null, $isUpgrade);
|
||||
}
|
||||
|
||||
@@ -298,7 +303,7 @@ class Install extends Action
|
||||
}
|
||||
}
|
||||
|
||||
public function prepareEnvironmentVariables(array $userInput, array $vars): array
|
||||
public function prepareEnvironmentVariables(array $userInput, array $vars, bool $shouldGenerateSecrets = true): array
|
||||
{
|
||||
$input = [];
|
||||
$password = new Password();
|
||||
@@ -309,12 +314,22 @@ class Install extends Action
|
||||
$default = $var['default'] ?? null;
|
||||
$hasDefault = $default !== null && $default !== '';
|
||||
if (!empty($var['filter']) && $var['filter'] === 'token') {
|
||||
$input[$var['name']] = $hasDefault ? $default : $token->generate();
|
||||
if ($hasDefault) {
|
||||
$input[$var['name']] = $default;
|
||||
} elseif ($shouldGenerateSecrets) {
|
||||
$input[$var['name']] = $token->generate();
|
||||
} else {
|
||||
$input[$var['name']] = '';
|
||||
}
|
||||
} elseif (!empty($var['filter']) && $var['filter'] === 'password') {
|
||||
/*;#+@:/?& broke DSNs locally */
|
||||
$input[$var['name']] = $hasDefault
|
||||
? $default
|
||||
: $this->generatePasswordValue($var['name'], $password);
|
||||
if ($hasDefault) {
|
||||
$input[$var['name']] = $default;
|
||||
} elseif ($shouldGenerateSecrets) {
|
||||
$input[$var['name']] = $this->generatePasswordValue($var['name'], $password);
|
||||
} else {
|
||||
$input[$var['name']] = '';
|
||||
}
|
||||
} else {
|
||||
$input[$var['name']] = $default;
|
||||
}
|
||||
@@ -349,6 +364,18 @@ class Install extends Action
|
||||
return $input;
|
||||
}
|
||||
|
||||
public function hasExistingConfig(): bool
|
||||
{
|
||||
$isLocalInstall = $this->isLocalInstall();
|
||||
$this->applyLocalPaths($isLocalInstall, true);
|
||||
|
||||
if ($this->readExistingCompose() !== '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return file_exists($this->path . '/' . $this->getEnvFileName());
|
||||
}
|
||||
|
||||
private function updateProgress(?callable $progress, string $step, string $status, array $messages = [], array $details = [], ?string $messageOverride = null): void
|
||||
{
|
||||
if (!$progress) {
|
||||
@@ -398,7 +425,8 @@ class Install extends Action
|
||||
bool $noStart,
|
||||
?callable $progress = null,
|
||||
?string $resumeFromStep = null,
|
||||
bool $isUpgrade = false
|
||||
bool $isUpgrade = false,
|
||||
array $account = []
|
||||
): void {
|
||||
$isLocalInstall = $this->isLocalInstall();
|
||||
$this->applyLocalPaths($isLocalInstall, false);
|
||||
@@ -412,8 +440,8 @@ class Install extends Action
|
||||
}
|
||||
|
||||
if ($isLocalInstall) {
|
||||
$organization = 'appwrite';
|
||||
$image = 'appwrite';
|
||||
$organization = 'appwrite';
|
||||
}
|
||||
|
||||
$templateForEnv = new View($this->buildFromProjectPath('/app/views/install/env.phtml'));
|
||||
@@ -469,7 +497,7 @@ class Install extends Action
|
||||
$this->updateProgress($progress, InstallerServer::STEP_DOCKER_COMPOSE, InstallerServer::STATUS_IN_PROGRESS, $messages);
|
||||
|
||||
if (!$useExistingConfig) {
|
||||
$this->writeComposeFile($templateForCompose, $isLocalInstall);
|
||||
$this->writeComposeFile($templateForCompose);
|
||||
}
|
||||
|
||||
$this->updateProgress($progress, InstallerServer::STEP_DOCKER_COMPOSE, InstallerServer::STATUS_COMPLETED, $messages);
|
||||
@@ -480,7 +508,7 @@ class Install extends Action
|
||||
$this->updateProgress($progress, InstallerServer::STEP_ENV_VARS, InstallerServer::STATUS_IN_PROGRESS, $messages);
|
||||
|
||||
if (!$useExistingConfig) {
|
||||
$this->writeEnvFile($templateForEnv, $isLocalInstall);
|
||||
$this->writeEnvFile($templateForEnv);
|
||||
}
|
||||
|
||||
$this->updateProgress($progress, InstallerServer::STEP_ENV_VARS, InstallerServer::STATUS_COMPLETED, $messages);
|
||||
@@ -495,9 +523,15 @@ class Install extends Action
|
||||
$currentStep = InstallerServer::STEP_DOCKER_CONTAINERS;
|
||||
$this->updateProgress($progress, InstallerServer::STEP_DOCKER_CONTAINERS, InstallerServer::STATUS_IN_PROGRESS, $messages);
|
||||
$this->runDockerCompose($input, $isLocalInstall, $useExistingConfig, $isCLI);
|
||||
$this->connectInstallerToAppwriteNetwork();
|
||||
|
||||
$this->updateProgress($progress, InstallerServer::STEP_DOCKER_CONTAINERS, InstallerServer::STATUS_COMPLETED, $messages);
|
||||
|
||||
if (!$isUpgrade) {
|
||||
// Create console account on fresh install only
|
||||
$this->createInitialAdminAccount($account, $progress);
|
||||
}
|
||||
|
||||
if ($isCLI) {
|
||||
Console::success('Appwrite installed successfully');
|
||||
}
|
||||
@@ -521,6 +555,135 @@ class Install extends Action
|
||||
}
|
||||
}
|
||||
|
||||
private function createInitialAdminAccount(array $account, ?callable $progress): void
|
||||
{
|
||||
$name = $account['name'] ?? 'Admin';
|
||||
$email = $account['email'] ?? null;
|
||||
$password = $account['password'] ?? null;
|
||||
|
||||
if (!$email || !$password) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->updateProgress(
|
||||
$progress,
|
||||
InstallerServer::STEP_ACCOUNT_SETUP,
|
||||
InstallerServer::STATUS_IN_PROGRESS,
|
||||
messageOverride: 'Creating Appwrite account'
|
||||
);
|
||||
|
||||
$this->waitForApiReady();
|
||||
|
||||
// Create account and session
|
||||
$userId = $this->makeApiCall('/v1/account', [
|
||||
'userId' => 'unique()',
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'name' => $name
|
||||
]);
|
||||
|
||||
$session = $this->makeApiCall('/v1/account/sessions/email', [
|
||||
'email' => $email,
|
||||
'password' => $password
|
||||
], true);
|
||||
|
||||
$this->updateProgress(
|
||||
$progress,
|
||||
InstallerServer::STEP_ACCOUNT_SETUP,
|
||||
InstallerServer::STATUS_COMPLETED,
|
||||
details: [
|
||||
'userId' => $userId,
|
||||
'sessionId' => $session['id'],
|
||||
'sessionSecret' => $session['secret'],
|
||||
'sessionExpire' => $session['expire'] ?? null
|
||||
],
|
||||
messageOverride: 'Account created successfully'
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$this->updateProgress(
|
||||
$progress,
|
||||
InstallerServer::STEP_ACCOUNT_SETUP,
|
||||
InstallerServer::STATUS_ERROR,
|
||||
messageOverride: 'Account creation failed: ' . $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function waitForApiReady(): void
|
||||
{
|
||||
$client = new Client();
|
||||
$client->setTimeout(5);
|
||||
|
||||
$pingHealth = function () use ($client): bool {
|
||||
try {
|
||||
return $client->fetch("http://traefik:80/v1/health/version")->getStatusCode() === 200;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
for ($i = 0; $i < self::HEALTH_CHECK_ATTEMPTS; $i++) {
|
||||
if ($pingHealth()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($i < self::HEALTH_CHECK_ATTEMPTS - 1) {
|
||||
sleep(self::HEALTH_CHECK_DELAY_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
throw new \Exception('Failed to connect with Appwrite in time');
|
||||
}
|
||||
|
||||
/* easier to just connect and call traefik endpoint */
|
||||
private function connectInstallerToAppwriteNetwork(): void
|
||||
{
|
||||
$network = escapeshellarg('appwrite');
|
||||
$container = escapeshellarg(InstallerServer::DEFAULT_CONTAINER);
|
||||
@exec("docker network connect $network $container 2>/dev/null");
|
||||
}
|
||||
|
||||
private function makeApiCall(string $endpoint, array $body, bool $extractSession = false)
|
||||
{
|
||||
$client = new Client();
|
||||
$client
|
||||
->setTimeout(30)
|
||||
->addHeader('Content-Type', 'application/json')
|
||||
->addHeader('X-Appwrite-Project', 'console');
|
||||
|
||||
$url = "http://traefik:80$endpoint";
|
||||
$response = $client->fetch($url, Client::METHOD_POST, $body);
|
||||
|
||||
if ($response->getStatusCode() !== 201) {
|
||||
$error = $response->json();
|
||||
$message = $error['message'] ?? 'Unknown error';
|
||||
throw new \Exception($message);
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
if (!isset($data['$id'])) {
|
||||
throw new \Exception('API response missing ID field');
|
||||
}
|
||||
|
||||
if ($extractSession) {
|
||||
$headers = $response->getHeaders();
|
||||
$setCookie = $headers['set-cookie'] ?? $headers['Set-Cookie'] ?? null;
|
||||
|
||||
if (!$setCookie || !preg_match('/a_session_console=([^;]+)/', $setCookie, $matches)) {
|
||||
throw new \Exception('Session created but no cookie found');
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $data['$id'],
|
||||
'secret' => $matches[1],
|
||||
'expire' => $data['expire'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
return $data['$id'];
|
||||
}
|
||||
|
||||
private function buildStepMessages(bool $isUpgrade): array
|
||||
{
|
||||
$isUpgradeLabel = $isUpgrade ? 'updated' : 'created';
|
||||
@@ -551,10 +714,8 @@ class Install extends Action
|
||||
];
|
||||
}
|
||||
|
||||
private function writeComposeFile(View $template, bool $isLocalInstall): void
|
||||
private function writeComposeFile(View $template): void
|
||||
{
|
||||
// Always use container path for file operations
|
||||
// Use a separate file for web installer to avoid conflicts
|
||||
$composeFileName = $this->getComposeFileName();
|
||||
$targetPath = $this->path . '/' . $composeFileName;
|
||||
$renderedContent = $template->render(false);
|
||||
@@ -567,10 +728,8 @@ class Install extends Action
|
||||
}
|
||||
}
|
||||
|
||||
private function writeEnvFile(View $template, bool $isLocalInstall): void
|
||||
private function writeEnvFile(View $template): void
|
||||
{
|
||||
// Always use container path for file operations
|
||||
// Use a separate env file for installer to avoid conflicts
|
||||
$envFileName = $this->getEnvFileName();
|
||||
if (!\file_put_contents($this->path . '/' . $envFileName, $template->render(false))) {
|
||||
throw new \Exception('Failed to save environment variables file');
|
||||
@@ -596,7 +755,7 @@ class Install extends Action
|
||||
continue;
|
||||
}
|
||||
if (!preg_match('/^[A-Z0-9_]+$/', $key)) {
|
||||
throw new \Exception('Invalid environment variable name');
|
||||
throw new \Exception("Invalid environment variable name: $key");
|
||||
}
|
||||
$env .= $key . '=' . \escapeshellarg((string) $value) . ' ';
|
||||
}
|
||||
@@ -606,8 +765,6 @@ class Install extends Action
|
||||
Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\"");
|
||||
}
|
||||
|
||||
// Docker compose runs inside container, so use container paths
|
||||
// The compose file itself contains host paths for volume mounts
|
||||
$composeFileName = $this->getComposeFileName();
|
||||
$composeFile = $this->path . '/' . $composeFileName;
|
||||
|
||||
@@ -645,7 +802,6 @@ class Install extends Action
|
||||
|
||||
protected function isLocalInstall(): bool
|
||||
{
|
||||
$config = null;
|
||||
if ($this->isLocalInstall === null) {
|
||||
$config = $this->readInstallerConfig();
|
||||
$this->isLocalInstall = !empty($config['isLocal']);
|
||||
|
||||
Reference in New Issue
Block a user