Files
gitea/web_src/js/features/install.ts
T
petru 471cfdd161
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
Modified - [install] [backup] [database] [recovery] Consolidated database backup and installer recovery support.
- 1 - Add: Gitea now creates timestamped database backup bundles under `[backup].PATH`, exposes the backup schedule in the installer, and surfaces the `database_backup` cron task in admin monitoring.
- 2 - Add: installed instances now use `.gitea-installed` and `.gitea-recovery.ini` to enter email-gated recovery instead of falling back to public install mode when configuration or database access is broken.
- 3 - Mod: the installer recovery flow now covers backup-bundle restore, bundled or manual `app.ini` reuse, uploaded SQL/GZ database restores, and repository-filesystem recovery with source-specific validation, confirmations, and preserved launcher state.
- 4 - Fix: recovery now restores bundled `app.ini` snapshots when needed, discovers backup bundles from both the active backup path and persisted `.gitea-recovery.ini` path, and preserves SMTP and other rebuilt settings correctly when `app.ini` is missing or incomplete.
- 5 - Fix: recovery validation and restore handling now accept either a selected backup bundle or an uploaded SQL/GZ dump, keep sensitive secrets and existing `LFS_JWT_SECRET` when appropriate, clear SQLite restore targets before import, and complete the post-install handoff without redirect loops.
- 6 - Mod: fresh installs now default recovery email authorization to enabled with first-admin fallback, and the install/recovery UI, styling, and EN/RO wording were refined to match the final launcher behavior.

Co-Authored-By: petru @ codex (GPT-5) <codex@openai.com>
(cherry picked from commit 9879caf2292691b0cb521d12e6fee924b066bae2)
2026-06-01 03:56:03 +03:00

196 lines
7.5 KiB
TypeScript

import {hideElem, showElem} from '../utils/dom.ts';
import {GET} from '../modules/fetch.ts';
export function initInstall() {
const page = document.querySelector('.page-content.install');
if (!page) {
return;
}
if (page.classList.contains('post-install')) {
initPostInstall();
} else {
initPreInstall();
}
}
function initPreInstall() {
const defaultDbUser = 'gitea';
const defaultDbName = 'gitea';
const defaultDbHosts: Record<string, string> = {
mysql: '127.0.0.1:3306',
postgres: '127.0.0.1:5432',
mssql: '127.0.0.1:1433',
};
const dbHost = document.querySelector<HTMLInputElement>('#db_host')!;
const dbUser = document.querySelector<HTMLInputElement>('#db_user')!;
const dbName = document.querySelector<HTMLInputElement>('#db_name')!;
// Database type change detection.
document.querySelector<HTMLInputElement>('#db_type')!.addEventListener('change', function () {
const dbType = this.value || 'mysql'; // edit/add - by petru @ codex
hideElem('div[data-db-setting-for]');
showElem(`div[data-db-setting-for="${dbType}"]`); // edit/add - by petru @ codex
if (dbType !== 'sqlite3') {
// for most remote database servers
showElem('div[data-db-setting-for="common-host"]'); // edit/add - by petru @ codex
const lastDbHost = dbHost.value;
const isDbHostDefault = !lastDbHost || Object.values(defaultDbHosts).includes(lastDbHost);
if (isDbHostDefault) {
dbHost.value = defaultDbHosts[dbType] ?? '';
}
if (!dbUser.value && !dbName.value) {
dbUser.value = defaultDbUser;
dbName.value = defaultDbName;
}
} // else: for SQLite3, the default path is always prepared by backend code (setting)
});
document.querySelector('#db_type')!.dispatchEvent(new Event('change'));
const appUrl = document.querySelector<HTMLInputElement>('#app_url')!;
if (appUrl.value.includes('://localhost')) {
// start edit/add - by petru @ codex
const currentUrl = new URL(window.location.href);
currentUrl.search = '';
currentUrl.hash = '';
appUrl.value = currentUrl.href;
// end edit/add - by petru @ codex
}
const domain = document.querySelector<HTMLInputElement>('#domain')!;
if (domain.value.trim() === 'localhost') {
domain.value = window.location.hostname;
}
const registrationModeInputs = document.querySelectorAll<HTMLInputElement>('input[name="registration_mode"]');
if (registrationModeInputs.length > 0) {
const registerConfirm = document.querySelector<HTMLInputElement>('input[name="register_confirm"]')!;
const registerManualConfirm = document.querySelector<HTMLInputElement>('input[name="register_manual_confirm"]')!;
const enableCaptcha = document.querySelector<HTMLInputElement>('input[name="enable_captcha"]')!;
const enableOpenIDSignIn = document.querySelector<HTMLInputElement>('input[name="enable_open_id_sign_in"]')!;
const enableOpenIDSignUp = document.querySelector<HTMLInputElement>('input[name="enable_open_id_sign_up"]')!;
const localOptions = document.querySelector<HTMLElement>('#registration-local-options')!;
const externalOptions = document.querySelector<HTMLElement>('#registration-external-options')!;
const getSelectedRegistrationMode = () => {
for (const input of registrationModeInputs) {
if (input.checked) {
return input.value;
}
}
return 'local_and_external';
};
const syncRegistrationManagement = () => {
const mode = getSelectedRegistrationMode();
const allowLocalRegistration = mode === 'local_only' || mode === 'local_and_external';
const allowExternalRegistration = mode === 'external_only' || mode === 'local_and_external';
if (allowLocalRegistration) {
showElem(localOptions);
} else {
hideElem(localOptions);
registerConfirm.checked = false;
registerManualConfirm.checked = false;
enableCaptcha.checked = false;
}
if (allowExternalRegistration) {
showElem(externalOptions);
} else {
hideElem(externalOptions);
enableOpenIDSignIn.checked = false;
enableOpenIDSignUp.checked = false;
}
registerManualConfirm.disabled = !allowLocalRegistration;
enableCaptcha.disabled = !allowLocalRegistration;
if (registerManualConfirm.checked) {
registerConfirm.checked = true;
registerConfirm.disabled = true;
} else {
registerConfirm.disabled = !allowLocalRegistration;
}
if (enableOpenIDSignUp.checked) {
enableOpenIDSignIn.checked = true;
}
if (!enableOpenIDSignIn.checked) {
enableOpenIDSignUp.checked = false;
}
enableOpenIDSignIn.disabled = !allowExternalRegistration;
enableOpenIDSignUp.disabled = !allowExternalRegistration || !enableOpenIDSignIn.checked;
};
for (const input of registrationModeInputs) {
input.addEventListener('change', syncRegistrationManagement);
}
registerManualConfirm.addEventListener('change', syncRegistrationManagement);
enableOpenIDSignIn.addEventListener('change', syncRegistrationManagement);
enableOpenIDSignUp.addEventListener('change', syncRegistrationManagement);
syncRegistrationManagement();
}
const brandingUseSharedAssets = document.querySelector<HTMLInputElement>('#branding_use_shared_assets');
const brandingFaviconFields = document.querySelectorAll<HTMLElement>('.js-install-branding-favicon-field');
const brandingLogoSvgLabel = document.querySelector<HTMLElement>('#branding_logo_svg_label');
const brandingLogoPngLabel = document.querySelector<HTMLElement>('#branding_logo_png_label');
if (brandingUseSharedAssets && brandingFaviconFields.length > 0) {
const syncBrandingLabels = () => {
if (brandingLogoSvgLabel) {
brandingLogoSvgLabel.textContent = brandingUseSharedAssets.checked ?
brandingLogoSvgLabel.getAttribute('data-shared-label') :
brandingLogoSvgLabel.getAttribute('data-default-label');
}
if (brandingLogoPngLabel) {
brandingLogoPngLabel.textContent = brandingUseSharedAssets.checked ?
brandingLogoPngLabel.getAttribute('data-shared-label') :
brandingLogoPngLabel.getAttribute('data-default-label');
}
};
const syncBrandingFields = () => {
if (brandingUseSharedAssets.checked) {
for (const field of brandingFaviconFields) hideElem(field);
} else {
for (const field of brandingFaviconFields) showElem(field);
}
syncBrandingLabels();
};
brandingUseSharedAssets.addEventListener('change', syncBrandingFields);
syncBrandingFields();
}
}
function initPostInstall() {
// start edit/add - by petru @ codex
if ((window as typeof window & {__giteaRecoveryPostInstallHandled?: boolean}).__giteaRecoveryPostInstallHandled) {
return;
}
// end edit/add - by petru @ codex
const el = document.querySelector<HTMLAnchorElement>('#goto-after-install');
if (!el) return;
const targetUrl = el.getAttribute('href')!;
const probeUrl = el.dataset.probeUrl || targetUrl;
const probeUntilMissing = el.dataset.probeUntilMissing === 'true';
let tid: ReturnType<typeof setInterval> | null = setInterval(async () => {
try {
const resp = await GET(probeUrl, {cache: 'no-store'});
const shouldRedirect = probeUntilMissing
? resp.status === 404 || resp.redirected || !resp.url.endsWith('/post-install')
: resp.status === 200;
if (tid && shouldRedirect) {
clearInterval(tid);
tid = null;
window.location.href = targetUrl;
}
} catch {}
}, 1000);
}