diff --git a/.gitignore b/.gitignore index d316597..8349bd2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ node_modules/ .env .env.docker *.log +**/bin/ +**/obj/ +.vs/ +artifacts/ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..0c8052e --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,9 @@ + + + latest + enable + enable + true + latest + + diff --git a/Eis.slnx b/Eis.slnx new file mode 100644 index 0000000..940e8a1 --- /dev/null +++ b/Eis.slnx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..c37e0d0 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,45 @@ +# ASP.NET Core 10 迁移 + +迁移采用兼容优先的渐进式方案:ASP.NET Core 作为统一入口,尚未迁移的 `/api/*` 请求暂时转发给运行在 `4174` 端口的 Node.js 服务。每完成一个功能域,就在 ASP.NET Core 中注册对应原生端点并停止转发该路径。 + +## 当前阶段 + +- [x] ASP.NET Core 10 解决方案与分层项目 +- [x] 原前端静态资源无修改托管 +- [x] 旧 API 兼容转发,包含 Cookie、请求体、文件下载和状态码 +- [x] 存活与迁移就绪检查 +- [ ] 公开接口与数据库读取 +- [ ] 登录、Session 与 TOTP +- [ ] 考生业务 +- [ ] 管理后台、审批流和考务编排 +- [ ] 招生录取 +- [ ] Excel、文书和缓存 +- [ ] 容器入口切换及 Node.js 后端移除 + +## 本地运行 + +先在一个终端运行旧 API: + +```powershell +$env:PORT = '4174' +npm start +``` + +再在另一个终端运行 ASP.NET Core 入口: + +```powershell +dotnet run --project .\src\Eis.Web\Eis.Web.csproj +``` + +浏览器仍访问 。 + +- `GET /health/live`:只检查 ASP.NET Core 宿主。 +- `GET /health/migration`:检查迁移宿主和旧 API 转发链路。 + +可通过配置 `LegacyNode:Enabled=false` 禁用兼容转发;此时尚未迁移的 API 会返回 `501`。 + +完整的宿主、静态资源、JSON 转发和 Session Cookie 冒烟测试: + +```powershell +pwsh.exe -NoLogo -NoProfile -NonInteractive -File .\scripts\smoke-dotnet-migration.ps1 +``` diff --git a/global.json b/global.json new file mode 100644 index 0000000..6d5813f --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.302", + "rollForward": "latestPatch", + "allowPrerelease": false + } +} diff --git a/scripts/smoke-dotnet-migration.ps1 b/scripts/smoke-dotnet-migration.ps1 new file mode 100644 index 0000000..0260df2 --- /dev/null +++ b/scripts/smoke-dotnet-migration.ps1 @@ -0,0 +1,182 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$repositoryRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +$temporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()) +$testDirectory = Join-Path $temporaryRoot ("eis-migration-smoke-{0}" -f [guid]::NewGuid().ToString('N')) +$nodeProcess = $null +$dotnetProcess = $null + +function Get-AvailableTcpPort { + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) + try { + $listener.Start() + return ([System.Net.IPEndPoint] $listener.LocalEndpoint).Port + } + finally { + $listener.Stop() + } +} + +function Start-TestProcess { + param( + [Parameter(Mandatory)] + [string] $FileName, + + [Parameter(Mandatory)] + [string[]] $ArgumentList, + + [Parameter(Mandatory)] + [hashtable] $Environment + ) + + $startInfo = [System.Diagnostics.ProcessStartInfo]::new() + $startInfo.FileName = $FileName + $startInfo.WorkingDirectory = $repositoryRoot + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + foreach ($argument in $ArgumentList) { + $startInfo.ArgumentList.Add($argument) + } + foreach ($entry in $Environment.GetEnumerator()) { + $startInfo.Environment[$entry.Key] = [string] $entry.Value + } + + return [System.Diagnostics.Process]::Start($startInfo) +} + +function Wait-ForUrl { + param( + [Parameter(Mandatory)] + [uri] $Uri, + + [Parameter(Mandatory)] + [System.Diagnostics.Process[]] $Processes + ) + + $deadline = [DateTimeOffset]::UtcNow.AddSeconds(25) + while ([DateTimeOffset]::UtcNow -lt $deadline) { + foreach ($process in $Processes) { + if ($process.HasExited) { + $standardOutput = $process.StandardOutput.ReadToEnd() + $standardError = $process.StandardError.ReadToEnd() + throw "Process $($process.Id) exited with code $($process.ExitCode) before $Uri became ready`n$standardOutput`n$standardError" + } + } + + try { + $response = Invoke-WebRequest -Uri $Uri -TimeoutSec 2 + if ($response.StatusCode -eq 200) { + return + } + } + catch { + Start-Sleep -Milliseconds 250 + } + } + + throw "Timed out waiting for $Uri" +} + +try { + New-Item -ItemType Directory -Path $testDirectory | Out-Null + + $legacyPort = Get-AvailableTcpPort + $webPort = Get-AvailableTcpPort + $legacyBaseUrl = "http://127.0.0.1:$legacyPort" + $webBaseUrl = "http://127.0.0.1:$webPort" + + $nodeExecutable = (Get-Command node.exe -ErrorAction Stop).Source + $dotnetExecutable = (Get-Command dotnet.exe -ErrorAction Stop).Source + $nodeProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @('server.mjs') -Environment @{ + NODE_ENV = 'test' + HOST = '127.0.0.1' + PORT = [string] $legacyPort + DATABASE_CLIENT = 'sqlite' + SQLITE_PATH = (Join-Path $testDirectory 'smoke.sqlite') + INITIAL_ADMIN_USERNAME = 'migration_admin' + INITIAL_ADMIN_PASSWORD = 'Migration123!' + INITIAL_ADMIN_DISPLAY_NAME = '迁移测试管理员' + REDIS_URL = '' + REDIS_SESSION_URL = '' + TOTP_ENCRYPTION_KEY = 'migration-smoke-totp-key-32-characters-minimum' + DOCUMENT_VERIFICATION_SECRET = 'migration-smoke-document-key-32-characters-minimum' + } + + $projectPath = Join-Path $repositoryRoot 'src\Eis.Web\Eis.Web.csproj' + $dotnetProcess = Start-TestProcess -FileName $dotnetExecutable -ArgumentList @( + 'run', + '--project', $projectPath, + '--configuration', 'Release', + '--no-build', + '--no-launch-profile', + '--', + '--urls', $webBaseUrl + ) -Environment @{ + ASPNETCORE_ENVIRONMENT = 'Development' + LegacyNode__Enabled = 'true' + LegacyNode__BaseUrl = $legacyBaseUrl + } + + Wait-ForUrl -Uri "$webBaseUrl/health/live" -Processes @($nodeProcess, $dotnetProcess) + Wait-ForUrl -Uri "$webBaseUrl/health/migration" -Processes @($nodeProcess, $dotnetProcess) + + $index = Invoke-WebRequest -Uri "$webBaseUrl/" + if ($index.Content -notmatch '衡准') { + throw 'ASP.NET Core did not serve the existing index.html' + } + + $clientModule = Invoke-WebRequest -Uri "$webBaseUrl/src/client/api.mjs" + if ($clientModule.Content -notmatch 'export function api') { + throw 'ASP.NET Core did not serve the existing client module' + } + + $homePayload = Invoke-RestMethod -Uri "$webBaseUrl/api/public/home" + if ($homePayload.ok -ne $true) { + throw 'Legacy public API proxy did not preserve the JSON response' + } + + $session = [Microsoft.PowerShell.Commands.WebRequestSession]::new() + $loginBody = @{ username = 'migration_admin'; password = 'Migration123!' } | ConvertTo-Json -Compress + $login = Invoke-RestMethod -Uri "$webBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $loginBody -WebSession $session + if ($login.ok -ne $true -or $login.user.username -ne 'migration_admin') { + throw 'Legacy login API proxy did not preserve the response' + } + + $currentUser = Invoke-RestMethod -Uri "$webBaseUrl/api/auth/me" -WebSession $session + if ($currentUser.user.username -ne 'migration_admin') { + throw 'Legacy API proxy did not preserve the session cookie' + } + + [pscustomobject]@{ + AspNetCoreHost = 'passed' + StaticAssets = 'passed' + JsonProxy = 'passed' + SessionCookie = 'passed' + } | Format-List +} +finally { + foreach ($process in @($dotnetProcess, $nodeProcess)) { + if ($null -ne $process -and -not $process.HasExited) { + $process.Kill($true) + $process.WaitForExit() + } + if ($null -ne $process) { + $process.Dispose() + } + } + + if (Test-Path -LiteralPath $testDirectory) { + $resolvedTestDirectory = [System.IO.Path]::GetFullPath($testDirectory) + if (-not $resolvedTestDirectory.StartsWith($temporaryRoot, [StringComparison]::OrdinalIgnoreCase) -or + -not ([System.IO.Path]::GetFileName($resolvedTestDirectory)).StartsWith('eis-migration-smoke-', [StringComparison]::Ordinal)) { + throw "Refusing to remove unexpected smoke-test directory: $resolvedTestDirectory" + } + Remove-Item -LiteralPath $resolvedTestDirectory -Recurse -Force + } +} diff --git a/src/Eis.Application/Eis.Application.csproj b/src/Eis.Application/Eis.Application.csproj new file mode 100644 index 0000000..3f03e4a --- /dev/null +++ b/src/Eis.Application/Eis.Application.csproj @@ -0,0 +1,9 @@ + + + net10.0 + Eis.Application + + + + + diff --git a/src/Eis.Application/Migration/MigrationFeature.cs b/src/Eis.Application/Migration/MigrationFeature.cs new file mode 100644 index 0000000..450a9f7 --- /dev/null +++ b/src/Eis.Application/Migration/MigrationFeature.cs @@ -0,0 +1,5 @@ +using Eis.Domain.Migration; + +namespace Eis.Application.Migration; + +public sealed record MigrationFeature(FeatureArea Area, bool Native, string RoutePrefix); diff --git a/src/Eis.Domain/Eis.Domain.csproj b/src/Eis.Domain/Eis.Domain.csproj new file mode 100644 index 0000000..e7b43dd --- /dev/null +++ b/src/Eis.Domain/Eis.Domain.csproj @@ -0,0 +1,6 @@ + + + net10.0 + Eis.Domain + + diff --git a/src/Eis.Domain/Migration/FeatureArea.cs b/src/Eis.Domain/Migration/FeatureArea.cs new file mode 100644 index 0000000..af4f8f0 --- /dev/null +++ b/src/Eis.Domain/Migration/FeatureArea.cs @@ -0,0 +1,16 @@ +namespace Eis.Domain.Migration; + +/// +/// Stable feature boundaries used while replacing the legacy implementation. +/// +public enum FeatureArea +{ + Public, + Authentication, + Candidate, + Administration, + Admission, + Documents, + Excel, + Caching +} diff --git a/src/Eis.Infrastructure/Eis.Infrastructure.csproj b/src/Eis.Infrastructure/Eis.Infrastructure.csproj new file mode 100644 index 0000000..7dcd91a --- /dev/null +++ b/src/Eis.Infrastructure/Eis.Infrastructure.csproj @@ -0,0 +1,10 @@ + + + net10.0 + Eis.Infrastructure + + + + + + diff --git a/src/Eis.Infrastructure/Migration/MigrationFeatureCatalog.cs b/src/Eis.Infrastructure/Migration/MigrationFeatureCatalog.cs new file mode 100644 index 0000000..d84571a --- /dev/null +++ b/src/Eis.Infrastructure/Migration/MigrationFeatureCatalog.cs @@ -0,0 +1,19 @@ +using Eis.Application.Migration; +using Eis.Domain.Migration; + +namespace Eis.Infrastructure.Migration; + +public static class MigrationFeatureCatalog +{ + public static IReadOnlyList Current { get; } = + [ + new(FeatureArea.Public, false, "/api/public"), + new(FeatureArea.Authentication, false, "/api/auth"), + new(FeatureArea.Candidate, false, "/api/candidate"), + new(FeatureArea.Administration, false, "/api/admin"), + new(FeatureArea.Admission, false, "/api/admission"), + new(FeatureArea.Documents, false, "/api"), + new(FeatureArea.Excel, false, "/api"), + new(FeatureArea.Caching, false, "/api") + ]; +} diff --git a/src/Eis.Web/Eis.Web.csproj b/src/Eis.Web/Eis.Web.csproj new file mode 100644 index 0000000..92c60fd --- /dev/null +++ b/src/Eis.Web/Eis.Web.csproj @@ -0,0 +1,22 @@ + + + net10.0 + Eis.Web + Eis.Web + + + + + + + + + + + + + + + + + diff --git a/src/Eis.Web/Frontend/FrontendAssets.cs b/src/Eis.Web/Frontend/FrontendAssets.cs new file mode 100644 index 0000000..360b21a --- /dev/null +++ b/src/Eis.Web/Frontend/FrontendAssets.cs @@ -0,0 +1,95 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.AspNetCore.StaticFiles; + +namespace Eis.Web.Frontend; + +public static class FrontendAssets +{ + private static readonly string[] ClientModules = + [ + "api.mjs", + "admin-views.mjs", + "candidate-views.mjs", + "admission-views.mjs", + "admission-plan-editor.mjs", + "public-views.mjs", + "state.mjs", + "ui.mjs", + "table-state.mjs", + "pdf-export.mjs", + "region-select.mjs" + ]; + + public static void MapFrontendAssets(this WebApplication app) + { + var repositoryRoot = FindRepositoryRoot(app.Environment.ContentRootPath); + if (repositoryRoot is null) + { + app.UseDefaultFiles(); + app.UseStaticFiles(CreateStaticOptions()); + app.MapFallbackToFile("index.html"); + return; + } + + var clientRoot = Path.Combine(repositoryRoot, "src", "client"); + foreach (var fileName in ClientModules) + { + MapFile(app, $"/src/client/{fileName}", Path.Combine(clientRoot, fileName), "text/javascript; charset=utf-8"); + } + + MapFile(app, "/src/data/china-regions.mjs", Path.Combine(repositoryRoot, "src", "data", "china-regions.mjs"), "text/javascript; charset=utf-8"); + MapFile(app, "/src/data/specialty-types.mjs", Path.Combine(repositoryRoot, "src", "data", "specialty-types.mjs"), "text/javascript; charset=utf-8"); + MapFile(app, "/styles.css", Path.Combine(repositoryRoot, "styles.css"), "text/css; charset=utf-8"); + MapFile(app, "/app.js", Path.Combine(repositoryRoot, "app.js"), "text/javascript; charset=utf-8"); + + var ckeditorRoot = Path.Combine(repositoryRoot, "node_modules", "ckeditor5", "dist"); + if (Directory.Exists(ckeditorRoot)) + { + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(Path.Combine(ckeditorRoot, "browser")), + RequestPath = "/vendor/ckeditor5", + OnPrepareResponse = SetNoCache + }); + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(Path.Combine(ckeditorRoot, "translations")), + RequestPath = "/vendor/ckeditor5/translations", + OnPrepareResponse = SetNoCache + }); + } + + var indexPath = Path.Combine(repositoryRoot, "index.html"); + MapFile(app, "/", indexPath, "text/html; charset=utf-8"); + MapFile(app, "/index.html", indexPath, "text/html; charset=utf-8"); + app.MapFallback(async context => + { + context.Response.ContentType = "text/html; charset=utf-8"; + context.Response.Headers.CacheControl = "no-cache"; + await context.Response.SendFileAsync(indexPath, context.RequestAborted); + }); + } + + private static void MapFile(WebApplication app, string route, string path, string contentType) => + app.MapGet(route, (HttpContext context) => + { + context.Response.Headers.CacheControl = "no-cache"; + return Results.File(path, contentType); + }); + + private static StaticFileOptions CreateStaticOptions() => new() + { + OnPrepareResponse = SetNoCache + }; + + private static void SetNoCache(StaticFileResponseContext context) => + context.Context.Response.Headers.CacheControl = "no-cache"; + + private static string? FindRepositoryRoot(string contentRoot) + { + var candidate = Path.GetFullPath(Path.Combine(contentRoot, "..", "..")); + return File.Exists(Path.Combine(candidate, "package.json")) && File.Exists(Path.Combine(candidate, "index.html")) + ? candidate + : null; + } +} diff --git a/src/Eis.Web/Legacy/LegacyApiProxy.cs b/src/Eis.Web/Legacy/LegacyApiProxy.cs new file mode 100644 index 0000000..3b23515 --- /dev/null +++ b/src/Eis.Web/Legacy/LegacyApiProxy.cs @@ -0,0 +1,135 @@ +using System.Net; +using System.Net.Http.Headers; +using Microsoft.Extensions.Options; + +namespace Eis.Web.Legacy; + +public sealed class LegacyApiProxy( + HttpClient httpClient, + IOptions options, + ILogger logger) +{ + private static readonly HashSet HopByHopHeaders = new(StringComparer.OrdinalIgnoreCase) + { + "Connection", + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Authorization", + "TE", + "Trailer", + "Transfer-Encoding", + "Upgrade" + }; + + private readonly LegacyNodeOptions _options = options.Value; + + public async Task ForwardAsync(HttpContext context) + { + if (!_options.Enabled) + { + context.Response.StatusCode = StatusCodes.Status501NotImplemented; + await context.Response.WriteAsJsonAsync(new + { + ok = false, + message = "该接口尚未迁移到 ASP.NET Core", + migration = new { native = false, legacyProxyEnabled = false } + }, context.RequestAborted); + return; + } + + var target = new Uri(_options.BaseUrl, $"{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}"); + using var outbound = CreateRequest(context, target); + + try + { + using var upstream = await httpClient.SendAsync( + outbound, + HttpCompletionOption.ResponseHeadersRead, + context.RequestAborted); + + context.Response.StatusCode = (int)upstream.StatusCode; + CopyResponseHeaders(upstream, context.Response); + + if (context.Request.Method != HttpMethods.Head && upstream.StatusCode != HttpStatusCode.NoContent) + { + await upstream.Content.CopyToAsync(context.Response.Body, context.RequestAborted); + } + } + catch (HttpRequestException exception) + { + logger.LogWarning(exception, "Legacy Node API at {Target} is unavailable", target); + if (context.Response.HasStarted) + { + context.Abort(); + return; + } + + context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + await context.Response.WriteAsJsonAsync(new + { + ok = false, + message = "迁移期间的旧版 API 服务暂时不可用", + migration = new { native = false, legacyProxyEnabled = true } + }, context.RequestAborted); + } + } + + public async Task IsAvailableAsync(CancellationToken cancellationToken) + { + if (!_options.Enabled) + { + return true; + } + + try + { + using var response = await httpClient.GetAsync("api/public/home", cancellationToken); + return response.IsSuccessStatusCode; + } + catch (HttpRequestException) + { + return false; + } + } + + private static HttpRequestMessage CreateRequest(HttpContext context, Uri target) + { + var request = new HttpRequestMessage(new HttpMethod(context.Request.Method), target); + var hasBody = context.Request.ContentLength > 0 || context.Request.Headers.TransferEncoding.Count > 0; + if (hasBody) + { + request.Content = new StreamContent(context.Request.Body); + } + + foreach (var (name, values) in context.Request.Headers) + { + if (name.Equals("Host", StringComparison.OrdinalIgnoreCase) || HopByHopHeaders.Contains(name)) + { + continue; + } + + var valueArray = values.ToArray(); + if (!request.Headers.TryAddWithoutValidation(name, valueArray) && request.Content is not null) + { + request.Content.Headers.TryAddWithoutValidation(name, valueArray); + } + } + + request.Headers.TryAddWithoutValidation("X-Forwarded-Host", context.Request.Host.Value); + request.Headers.TryAddWithoutValidation("X-Forwarded-Proto", context.Request.Scheme); + return request; + } + + private static void CopyResponseHeaders(HttpResponseMessage upstream, HttpResponse response) + { + foreach (var header in upstream.Headers.Concat(upstream.Content.Headers)) + { + if (!HopByHopHeaders.Contains(header.Key)) + { + response.Headers.Append(header.Key, header.Value.ToArray()); + } + } + + response.Headers.Remove("transfer-encoding"); + } +} diff --git a/src/Eis.Web/Legacy/LegacyNodeOptions.cs b/src/Eis.Web/Legacy/LegacyNodeOptions.cs new file mode 100644 index 0000000..7a79bf2 --- /dev/null +++ b/src/Eis.Web/Legacy/LegacyNodeOptions.cs @@ -0,0 +1,10 @@ +namespace Eis.Web.Legacy; + +public sealed class LegacyNodeOptions +{ + public const string SectionName = "LegacyNode"; + + public bool Enabled { get; init; } = true; + + public Uri BaseUrl { get; init; } = new("http://127.0.0.1:4174"); +} diff --git a/src/Eis.Web/Program.cs b/src/Eis.Web/Program.cs new file mode 100644 index 0000000..6193737 --- /dev/null +++ b/src/Eis.Web/Program.cs @@ -0,0 +1,70 @@ +using System.Net; +using Eis.Infrastructure.Migration; +using Eis.Web.Frontend; +using Eis.Web.Legacy; + +var builder = WebApplication.CreateBuilder(args); + +builder.WebHost.ConfigureKestrel(options => options.AddServerHeader = false); +builder.Services.Configure(builder.Configuration.GetSection(LegacyNodeOptions.SectionName)); +builder.Services.AddHttpClient((services, client) => +{ + var options = services.GetRequiredService>().Value; + client.BaseAddress = options.BaseUrl; + client.Timeout = TimeSpan.FromSeconds(30); + client.DefaultRequestHeaders.UserAgent.ParseAdd("Eis.AspNetCore.Migration/1.0"); +}).ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler +{ + AllowAutoRedirect = false, + AutomaticDecompression = DecompressionMethods.None, + UseCookies = false +}); +builder.Services.AddProblemDetails(); + +var app = builder.Build(); + +app.UseExceptionHandler(); +app.Use(async (context, next) => +{ + context.Response.Headers.XContentTypeOptions = "nosniff"; + context.Response.Headers.XFrameOptions = "DENY"; + context.Response.Headers["Referrer-Policy"] = "same-origin"; + await next(); +}); + +app.MapGet("/health/live", () => Results.Json(new +{ + status = "healthy", + service = "Eis.Web", + framework = ".NET 10" +})); + +app.MapGet("/health/migration", async (LegacyApiProxy proxy, CancellationToken cancellationToken) => +{ + var legacyAvailable = await proxy.IsAvailableAsync(cancellationToken); + var statusCode = legacyAvailable ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable; + return Results.Json(new + { + status = legacyAvailable ? "healthy" : "degraded", + legacyApiAvailable = legacyAvailable, + features = MigrationFeatureCatalog.Current + }, statusCode: statusCode); +}); + +string[] methods = +[ + HttpMethods.Get, + HttpMethods.Head, + HttpMethods.Post, + HttpMethods.Put, + HttpMethods.Patch, + HttpMethods.Delete, + HttpMethods.Options +]; +app.MapMethods("/api/{**path}", methods, (HttpContext context, LegacyApiProxy proxy) => proxy.ForwardAsync(context)); + +app.MapFrontendAssets(); + +app.Run(); + +public partial class Program; diff --git a/src/Eis.Web/Properties/launchSettings.json b/src/Eis.Web/Properties/launchSettings.json new file mode 100644 index 0000000..fc90fd7 --- /dev/null +++ b/src/Eis.Web/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://127.0.0.1:4173", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Eis.Web/appsettings.json b/src/Eis.Web/appsettings.json new file mode 100644 index 0000000..3750199 --- /dev/null +++ b/src/Eis.Web/appsettings.json @@ -0,0 +1,13 @@ +{ + "LegacyNode": { + "Enabled": true, + "BaseUrl": "http://127.0.0.1:4174" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Eis.Web/wwwroot/.gitkeep b/src/Eis.Web/wwwroot/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Eis.Web/wwwroot/.gitkeep @@ -0,0 +1 @@ +