471cfdd161
- 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)
196 lines
7.5 KiB
TypeScript
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);
|
|
}
|