From 3446691162b21889f1383295c3fa28aa42ac36e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:37:52 +0300 Subject: [PATCH] Database Setup, raporlama, Demo, BackgroundWorker, --- ...DatabaseLocalizationResourceContributor.cs | 21 ++-- ...BackgroundWorkerRecurringJobInitializer.cs | 60 ++++++++++ .../BackgroundWorkerStartupService.cs | 44 +++++-- .../Controllers/SetupController.cs | 41 ++++++- .../DbStartup/SetupAppRunner.cs | 21 ++++ .../PlatformSessionValidationMiddleware.cs | 38 ++++-- .../PlatformHttpApiHostModule.cs | 6 +- .../PredefinedReports/DynamicGridReport.cs | 7 +- .../appsettings.json | 3 + ui/src/contexts/DemoContext.tsx | 6 + ui/src/views/list/Grid.tsx | 6 +- ui/src/views/list/Tree.tsx | 6 +- ui/src/views/public/Home.tsx | 4 +- ui/src/views/setup/DatabaseSetup.tsx | 109 ++++++++++++++++-- 14 files changed, 327 insertions(+), 45 deletions(-) create mode 100644 api/src/Sozsoft.Platform.HttpApi.Host/AbpBackgroundWorkerRecurringJobInitializer.cs diff --git a/api/modules/Sozsoft.Languages/Sozsoft.Languages.Domain/Localization/DatabaseLocalizationResourceContributor.cs b/api/modules/Sozsoft.Languages/Sozsoft.Languages.Domain/Localization/DatabaseLocalizationResourceContributor.cs index 13184d8..3b90a53 100644 --- a/api/modules/Sozsoft.Languages/Sozsoft.Languages.Domain/Localization/DatabaseLocalizationResourceContributor.cs +++ b/api/modules/Sozsoft.Languages/Sozsoft.Languages.Domain/Localization/DatabaseLocalizationResourceContributor.cs @@ -10,36 +10,41 @@ namespace Sozsoft.Languages.Localization; public class DatabaseLocalizationResourceContributor : ILocalizationResourceContributor { private LocalizationResourceBase _resource; - private IDatabaseResourceLocalizer _databaseResourceLocalizer; - private ILanguageProvider _languageProvider; + private System.Lazy _databaseResourceLocalizer; + private System.Lazy _languageProvider; public bool IsDynamic => false; public void Initialize(LocalizationResourceInitializationContext context) { _resource = context.Resource; - _databaseResourceLocalizer = context.ServiceProvider.GetRequiredService(); - _languageProvider = context.ServiceProvider.GetRequiredService(); + // ABP calls Initialize while AbpStringLocalizerFactory holds its internal lock. + // Resolving repositories/caches here can request another IStringLocalizer and + // re-enter the same lock, preventing Kestrel from ever starting. + _databaseResourceLocalizer = new System.Lazy( + () => context.ServiceProvider.GetRequiredService()); + _languageProvider = new System.Lazy( + () => context.ServiceProvider.GetRequiredService()); } public LocalizedString GetOrNull(string cultureName, string name) { - return _databaseResourceLocalizer.GetOrNull(_resource, cultureName, name); + return _databaseResourceLocalizer.Value.GetOrNull(_resource, cultureName, name); } public async Task FillAsync(string cultureName, Dictionary dictionary) { - await _databaseResourceLocalizer.FillAsync(_resource, cultureName, dictionary); + await _databaseResourceLocalizer.Value.FillAsync(_resource, cultureName, dictionary); } public void Fill(string cultureName, Dictionary dictionary) { - _databaseResourceLocalizer.Fill(_resource, cultureName, dictionary); + _databaseResourceLocalizer.Value.Fill(_resource, cultureName, dictionary); } public async Task> GetSupportedCulturesAsync() { - var langs = await _languageProvider.GetLanguagesAsync(); + var langs = await _languageProvider.Value.GetLanguagesAsync(); return langs.Select(a => a.CultureName).ToList(); } } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/AbpBackgroundWorkerRecurringJobInitializer.cs b/api/src/Sozsoft.Platform.HttpApi.Host/AbpBackgroundWorkerRecurringJobInitializer.cs new file mode 100644 index 0000000..78d3d87 --- /dev/null +++ b/api/src/Sozsoft.Platform.HttpApi.Host/AbpBackgroundWorkerRecurringJobInitializer.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.DependencyInjection; +using Volo.Abp.OpenIddict.Tokens; + +namespace Sozsoft.Platform; + +public class AbpBackgroundWorkerRecurringJobInitializer : ITransientDependency +{ + private readonly IServiceProvider _serviceProvider; + private readonly IBackgroundWorkerManager _backgroundWorkerManager; + private readonly ILogger _logger; + + public AbpBackgroundWorkerRecurringJobInitializer( + IServiceProvider serviceProvider, + IBackgroundWorkerManager backgroundWorkerManager, + ILogger logger) + { + _serviceProvider = serviceProvider; + _backgroundWorkerManager = backgroundWorkerManager; + _logger = logger; + } + + public async Task RunAsync(CancellationToken cancellationToken = default) + { + await AddWorkerIfAvailableAsync( + nameof(BackgroundJobWorker), + cancellationToken); + + await AddWorkerIfAvailableAsync( + nameof(TokenCleanupBackgroundWorker), + cancellationToken); + } + + private async Task AddWorkerIfAvailableAsync( + string workerName, + CancellationToken cancellationToken) + where TWorker : class, IBackgroundWorker + { + var worker = _serviceProvider.GetService(); + if (worker == null) + { + _logger.LogWarning( + "ABP background worker {WorkerName} could not be resolved from DI. Recurring job was not registered.", + workerName); + return; + } + + await _backgroundWorkerManager.AddAsync(worker, cancellationToken); + + _logger.LogInformation( + "ABP background worker {WorkerName} recurring job registration has been ensured.", + workerName); + } +} diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/BackgroundWorkerStartupService.cs b/api/src/Sozsoft.Platform.HttpApi.Host/BackgroundWorkerStartupService.cs index f45c6d4..6ff6903 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/BackgroundWorkerStartupService.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/BackgroundWorkerStartupService.cs @@ -40,19 +40,41 @@ public class BackgroundWorkerStartupService : IHostedService private async Task InitializeBackgroundWorkersAsync() { - try - { - _logger.LogInformation("Background worker startup initialization started."); + const int maxAttempts = 5; - using var scope = _serviceProvider.CreateScope(); - var initializer = scope.ServiceProvider.GetRequiredService(); - await initializer.RunAsync(); - - _logger.LogInformation("Background worker startup initialization completed."); - } - catch (Exception ex) + for (var attempt = 1; attempt <= maxAttempts; attempt++) { - _logger.LogError(ex, "Background worker initialization failed."); + try + { + _logger.LogInformation( + "Background worker startup initialization started. Attempt {Attempt}/{MaxAttempts}.", + attempt, + maxAttempts); + + using var scope = _serviceProvider.CreateScope(); + var abpRecurringJobInitializer = scope.ServiceProvider.GetRequiredService(); + await abpRecurringJobInitializer.RunAsync(); + + var initializer = scope.ServiceProvider.GetRequiredService(); + await initializer.RunAsync(); + + _logger.LogInformation("Background worker startup initialization completed."); + return; + } + catch (Exception ex) + { + if (attempt == maxAttempts) + { + _logger.LogError(ex, "Background worker initialization failed."); + return; + } + + _logger.LogWarning( + ex, + "Background worker initialization failed. It will be retried."); + + await Task.Delay(TimeSpan.FromSeconds(3)); + } } } } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs index 8a9eeba..52573ec 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Controllers/SetupController.cs @@ -21,13 +21,19 @@ public class SetupController : ControllerBase { private readonly IConfiguration _configuration; private readonly IHostEnvironment _env; + private readonly BackgroundWorkerInitializer _backgroundWorkerInitializer; + private readonly AbpBackgroundWorkerRecurringJobInitializer _abpBackgroundWorkerRecurringJobInitializer; public SetupController( IConfiguration configuration, - IHostEnvironment env) + IHostEnvironment env, + BackgroundWorkerInitializer backgroundWorkerInitializer, + AbpBackgroundWorkerRecurringJobInitializer abpBackgroundWorkerRecurringJobInitializer) { _configuration = configuration; _env = env; + _backgroundWorkerInitializer = backgroundWorkerInitializer; + _abpBackgroundWorkerRecurringJobInitializer = abpBackgroundWorkerRecurringJobInitializer; } [HttpGet("application-status")] @@ -38,8 +44,24 @@ public class SetupController : ControllerBase } [HttpPost("migrate")] + [AllowAnonymous] public async Task Migrate(CancellationToken ct) { + var databaseIsReady = SetupAppRunner.DatabaseIsReady(_configuration); + if (databaseIsReady) + { + if (User?.IsInRole("admin") != true) + { + Response.StatusCode = StatusCodes.Status403Forbidden; + return; + } + } + else if (!SetupAppRunner.IsValidSetupPassword(_configuration, Request.Headers)) + { + Response.StatusCode = StatusCodes.Status403Forbidden; + return; + } + Response.ContentType = "text/event-stream; charset=utf-8"; Response.Headers["Cache-Control"] = "no-cache, no-store"; Response.Headers["X-Accel-Buffering"] = "no"; @@ -141,7 +163,22 @@ public class SetupController : ControllerBase if (process.ExitCode == 0) { - await Send("success", "Migration and seed completed successfully."); + await Send("info", "Migration and seed completed successfully."); + + try + { + await Send("info", "Loading background workers into Hangfire..."); + await _abpBackgroundWorkerRecurringJobInitializer.RunAsync(ct); + await _backgroundWorkerInitializer.RunAsync(); + await Send("success", "Background workers loaded successfully."); + } + catch (Exception ex) + { + await Send("error", $"Background workers could not be loaded: {ex.Message}"); + await Send("done", "Failed."); + return; + } + await Send("done", "Completed."); } else diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupAppRunner.cs b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupAppRunner.cs index dae9478..9f3ad36 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupAppRunner.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/DbStartup/SetupAppRunner.cs @@ -22,6 +22,8 @@ namespace Sozsoft.Platform; /// internal static class SetupAppRunner { + private const string SetupPasswordHeaderName = "X-Setup-Password"; + // Veritabanı Hazırlık Kontrolü /// @@ -51,6 +53,18 @@ internal static class SetupAppRunner } } + public static bool IsValidSetupPassword(IConfiguration configuration, IHeaderDictionary headers) + { + var configuredPassword = configuration["Setup:MigrationPassword"]; + if (string.IsNullOrWhiteSpace(configuredPassword)) + { + return false; + } + + return headers.TryGetValue(SetupPasswordHeaderName, out var providedPassword) && + string.Equals(providedPassword.ToString(), configuredPassword, StringComparison.Ordinal); + } + private static bool SqlServerIsReady(string connectionString) { var csb = new SqlConnectionStringBuilder(connectionString); @@ -175,6 +189,13 @@ internal static class SetupAppRunner app.MapPost("/api/setup/migrate", async (IConfiguration cfg, IHostEnvironment env, IHostApplicationLifetime lifetime, HttpContext ctx, CancellationToken ct) => { + if (!IsValidSetupPassword(cfg, ctx.Request.Headers)) + { + ctx.Response.StatusCode = StatusCodes.Status403Forbidden; + await ctx.Response.WriteAsync("Invalid setup password.", ct); + return; + } + ctx.Response.ContentType = "text/event-stream; charset=utf-8"; ctx.Response.Headers["Cache-Control"] = "no-cache, no-store"; ctx.Response.Headers["X-Accel-Buffering"] = "no"; diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionValidationMiddleware.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionValidationMiddleware.cs index b9bb36c..2dad386 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionValidationMiddleware.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionValidationMiddleware.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Microsoft.Data.SqlClient; using Microsoft.AspNetCore.Http; using Volo.Abp; using Volo.Abp.DependencyInjection; @@ -29,6 +30,12 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend public async Task InvokeAsync(HttpContext context, RequestDelegate next) { + if (context.Request.Path.StartsWithSegments("/api/setup")) + { + await next(context); + return; + } + if (context.User.Identity?.IsAuthenticated != true) { await next(context); @@ -47,14 +54,22 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend var tenantId = ParseTenantId(context.User.FindFirst(AbpClaimTypes.TenantId)?.Value); using (currentTenant.Change(tenantId)) { - using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); - var sessions = await sessionRepository.GetListAsync(userId: userId); - var isCurrentSession = sessions.Any(session => session.SessionId == sessionId); - await uow.CompleteAsync(); - - if (!isCurrentSession) + try { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); + var sessions = await sessionRepository.GetListAsync(userId: userId); + var isCurrentSession = sessions.Any(session => session.SessionId == sessionId); + await uow.CompleteAsync(); + + if (!isCurrentSession) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return; + } + } + catch (Exception ex) when (IsDatabaseUnavailableException(ex)) + { + context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; return; } } @@ -66,4 +81,13 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend { return !string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out var id) ? id : null; } + + private static bool IsDatabaseUnavailableException(Exception exception) + { + return exception is SqlException || + exception is TimeoutException || + exception is TaskCanceledException || + exception is InvalidOperationException && exception.InnerException != null && + IsDatabaseUnavailableException(exception.InnerException); + } } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs index 5d333b7..5743ddc 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs @@ -342,7 +342,11 @@ public class PlatformHttpApiHostModule : AbpModule private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration) { var connectionString = configuration.GetConnectionString(DefaultDatabaseProvider); - if (connectionString.IsNullOrWhiteSpace() || !SetupAppRunner.DatabaseIsReady(configuration)) + // Program has already selected the full application only after the database + // readiness check succeeds. Repeating the probe here creates a race: one + // transient failure skips AddHangfire while ABP still starts its Hangfire + // modules, causing JobStorage initialization to crash the host. + if (connectionString.IsNullOrWhiteSpace()) { return; } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs index 429194b..2895e64 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs @@ -152,7 +152,7 @@ public class DynamicGridReport : XtraReport localizer); reportColumns = await ApplyImageCountsAsync(reportColumns, dynamicDataRepository, selectQuery, connectionString, imageBaseUrl); - ConfigurePage(reportColumns); + ConfigurePage(reportColumns, isSubReport); ConfigureDataSource( selectQuery, connectionString, @@ -162,13 +162,14 @@ public class DynamicGridReport : XtraReport } #pragma warning disable CA1416 - private void ConfigurePage(IReadOnlyCollection visibleFields) + private void ConfigurePage(IReadOnlyCollection visibleFields, bool isSubReport) { Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35); PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4; var portraitContentWidth = A4PortraitWidth - Margins.Left - Margins.Right; var requestedContentWidth = visibleFields.Sum(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)); - Landscape = visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth; + Landscape = !isSubReport && + (visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth); Font = new DXFont("Arial", 8.25F); } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.json b/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.json index 4557efb..46de1be 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.json +++ b/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.json @@ -29,6 +29,9 @@ "RefreshTokenMinutes": 90 } }, + "Setup": { + "MigrationPassword": "sozsoft" + }, "StringEncryption": { "DefaultPassPhrase": "UQpiYfT79zRZ3yYH" }, diff --git a/ui/src/contexts/DemoContext.tsx b/ui/src/contexts/DemoContext.tsx index 6735ce4..369cd0e 100644 --- a/ui/src/contexts/DemoContext.tsx +++ b/ui/src/contexts/DemoContext.tsx @@ -17,3 +17,9 @@ export const useDemo = () => { return context } + +export const useOptionalDemo = () => { + const context = useContext(DemoContext) + + return context ?? { openDemo: () => undefined } +} diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx index 8274d26..f173130 100644 --- a/ui/src/views/list/Grid.tsx +++ b/ui/src/views/list/Grid.tsx @@ -270,6 +270,10 @@ const Grid = (props: GridProps) => { const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { translate } = useLocalization() const currentUser = useStoreState((state) => state.auth.user) + const currentCulture = useStoreState( + (state) => state.abpConfig.config?.localization.currentCulture.cultureName, + ) + const localizationTexts = useStoreState((state) => state.abpConfig.texts) const useMobileEditPopup = isMobileViewport() || isTouchLikeDevice() const gridRef = useRef() @@ -1096,7 +1100,7 @@ const Grid = (props: GridProps) => { }) return cols - }, [gridDto]) + }, [gridDto, currentCulture, localizationTexts]) useEffect(() => { setColumnData(memoizedColumns) diff --git a/ui/src/views/list/Tree.tsx b/ui/src/views/list/Tree.tsx index e9d1200..ae5f41f 100644 --- a/ui/src/views/list/Tree.tsx +++ b/ui/src/views/list/Tree.tsx @@ -258,6 +258,10 @@ const Tree = (props: TreeProps) => { const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { translate } = useLocalization() const currentUser = useStoreState((state) => state.auth.user) + const currentCulture = useStoreState( + (state) => state.abpConfig.config?.localization.currentCulture.cultureName, + ) + const localizationTexts = useStoreState((state) => state.abpConfig.texts) const useMobileEditPopup = isMobileViewport() || isTouchLikeDevice() const gridRef = useRef() @@ -971,7 +975,7 @@ const Tree = (props: TreeProps) => { cols, ) setTreeListDataSource(dataSource) - }, [gridDto, searchParams]) + }, [gridDto, searchParams, currentCulture, localizationTexts]) useEffect(() => { const activeFilters = extraFilters.filter((f) => f.value) diff --git a/ui/src/views/public/Home.tsx b/ui/src/views/public/Home.tsx index 54bbca2..7030e55 100644 --- a/ui/src/views/public/Home.tsx +++ b/ui/src/views/public/Home.tsx @@ -18,7 +18,7 @@ import { DesignerSelection } from './designer/types' import { useDesignerState } from './designer/useDesignerState' import { getHome, HomeDto, saveHomePage } from '@/services/home.service' import { Button, Notification, toast } from '@/components/ui' -import { useDemo } from '@/contexts/DemoContext' +import { useOptionalDemo } from '@/contexts/DemoContext' interface HomeSlideServiceContent { icon: string @@ -273,7 +273,7 @@ function buildHomeContent(home: HomeDto | undefined, translate: (key: string) => } const Home: React.FC = () => { - const { openDemo } = useDemo() + const { openDemo } = useOptionalDemo() const { translate } = useLocalization() const { setLang } = useStoreActions((actions) => actions.locale) const { getConfig } = useStoreActions((actions) => actions.abpConfig) diff --git a/ui/src/views/setup/DatabaseSetup.tsx b/ui/src/views/setup/DatabaseSetup.tsx index bdb10d9..2723c45 100644 --- a/ui/src/views/setup/DatabaseSetup.tsx +++ b/ui/src/views/setup/DatabaseSetup.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { APP_NAME } from '@/constants/app.constant' import { getMigrateUrl, getSetupStatus } from '@/services/setup.service' import { applicationConfigurationUrl } from '@/services/abpConfig.service' -import { Button } from '@/components/ui' +import { Button, Dialog } from '@/components/ui' interface LogLine { level: 'info' | 'warn' | 'error' | 'success' | 'restart' | 'done' @@ -79,6 +79,8 @@ const DatabaseSetup = () => { const [status, setStatus] = useState('idle') const [pollCountdown, setPollCountdown] = useState(0) const [dbExists, setDbExists] = useState(null) + const [setupPassword, setSetupPassword] = useState('') + const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false) const logEndRef = useRef(null) const pollTimerRef = useRef | null>(null) @@ -119,9 +121,15 @@ const DatabaseSetup = () => { const startMigration = async () => { if (status === 'running') return + if (!setupPassword.trim()) { + setStatus('error') + addLog('error', 'Setup password is required.') + return + } setLogs([]) setStatus('running') + setIsPasswordDialogOpen(false) addLog('info', 'Starting migration...') const url = getMigrateUrl() @@ -152,6 +160,7 @@ const DatabaseSetup = () => { if (level === 'success') { setStatus('success') + pollUntilReady() } else if (level === 'error') { setStatus('error') } else if (level === 'restart') { @@ -166,12 +175,20 @@ const DatabaseSetup = () => { try { const response = await fetch(url, { method: 'POST', - headers: { Accept: 'text/event-stream' }, + headers: { + Accept: 'text/event-stream', + 'X-Setup-Password': setupPassword, + }, signal: abortController.signal, }) if (!response.ok || !response.body) { - addLog('error', `Server responded with status ${response.status}`) + addLog( + 'error', + response.status === 403 + ? 'Invalid setup password or setup migration is not allowed.' + : `Server responded with status ${response.status}`, + ) setStatus('error') return } @@ -196,9 +213,29 @@ const DatabaseSetup = () => { return (
{/* Header */} -
-

{APP_NAME}

-

Initial Setup — Creating Database

+
+
+
+
+
+ {`${APP_NAME} +
+
+ +
+

+ {APP_NAME} +

+
+ + Database creation and migration workspace +
+
+
{/* Status Card */} @@ -234,7 +271,7 @@ const DatabaseSetup = () => {
{/* Action Area */} -
+
{status === 'idle' && dbExists === true && 'Database already exists. Migration is not required.'} {status === 'idle' && dbExists === false && 'Database not found. Press the button to start migration.'} @@ -251,9 +288,10 @@ const DatabaseSetup = () => { {(status === 'idle' || status === 'error') && !dbExists && ( )} @@ -267,6 +305,59 @@ const DatabaseSetup = () => {
+ setIsPasswordDialogOpen(false)} + onRequestClose={() => setIsPasswordDialogOpen(false)} + width={440} + > + +
+
Setup authorization
+

+ Enter the setup password to start database migration. +

+
+
+ + + setSetupPassword(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter' && setupPassword.trim()) { + startMigration() + } + }} + placeholder="Enter setup password" + autoComplete="off" + className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500" + /> +

+ This protects the migration action when users are redirected to setup after a database + problem. +

+
+ + + + +
+

This page is only visible when the database does not exist.