@@ -1,10 +1,17 @@
|
||||
# .env.template - Template of environment variables for docker-compose
|
||||
# Copy this file to `.env.production` or `.env.staging` and fill the secret values.
|
||||
# Copy this file to `.env` (local), `.env.staging`, or `.env.production` and fill the secret values.
|
||||
# Do NOT commit your `.env.*` files containing real secrets.
|
||||
|
||||
# Common
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
|
||||
# Web (myai-web container) - maps to web appsettings Site:Mode section
|
||||
# Controls the public site experience:
|
||||
# Normal - full site (default)
|
||||
# UnderConstruction - redirect visitors to /under-construction.html
|
||||
# Unavailable - redirect visitors to /site-unavailable.html (HTTP 503)
|
||||
Site__Mode=Normal
|
||||
|
||||
# API (main)
|
||||
ASPNETCORE_URLS=http://+:8080
|
||||
APP_ENVIRONMENT_NAME=myai-Development
|
||||
|
||||
@@ -260,6 +260,9 @@ services:
|
||||
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Production}
|
||||
- ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080}
|
||||
- APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.production}
|
||||
|
||||
# Site: matches web appsettings Site section (Normal, UnderConstruction, Unavailable)
|
||||
- Site__Mode=${Site__Mode:-Normal}
|
||||
networks:
|
||||
- myai-network
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -260,6 +260,9 @@ services:
|
||||
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
|
||||
- ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080}
|
||||
- APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.staging}
|
||||
|
||||
# Site: matches web appsettings Site section (Normal, UnderConstruction, Unavailable)
|
||||
- Site__Mode=${Site__Mode:-Normal}
|
||||
networks:
|
||||
- myai-network
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -289,6 +289,9 @@ services:
|
||||
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Development}
|
||||
- ASPNETCORE_URLS=${ASPNETCORE_URLS:-http://+:8080}
|
||||
- APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.local}
|
||||
|
||||
# Site: matches web appsettings Site section (Normal, UnderConstruction, Unavailable)
|
||||
- Site__Mode=${Site__Mode:-Normal}
|
||||
networks:
|
||||
- myai-network
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Web.Settings;
|
||||
|
||||
namespace Web.Middleware;
|
||||
|
||||
public sealed class SiteModeMiddleware(RequestDelegate next, IOptions<SiteSettings> options)
|
||||
{
|
||||
private static readonly string[] StaticAssetPrefixes =
|
||||
[
|
||||
"/css/",
|
||||
"/js/",
|
||||
"/img/",
|
||||
"/logo/",
|
||||
"/fonts/"
|
||||
];
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var mode = ResolveMode(options.Value.Mode);
|
||||
if (mode == SiteModes.Normal)
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var path = context.Request.Path.Value ?? "/";
|
||||
var statusPath = GetStatusPath(mode);
|
||||
|
||||
if (IsAllowedPath(path, statusPath))
|
||||
{
|
||||
if (mode == SiteModes.Unavailable &&
|
||||
path.Equals("/site-unavailable.html", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||
context.Response.Headers.RetryAfter = "3600";
|
||||
}
|
||||
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
context.Response.Redirect(statusPath);
|
||||
}
|
||||
|
||||
private static string ResolveMode(string? configuredMode)
|
||||
{
|
||||
if (string.Equals(configuredMode, SiteModes.UnderConstruction, StringComparison.OrdinalIgnoreCase))
|
||||
return SiteModes.UnderConstruction;
|
||||
|
||||
if (string.Equals(configuredMode, SiteModes.Unavailable, StringComparison.OrdinalIgnoreCase))
|
||||
return SiteModes.Unavailable;
|
||||
|
||||
return SiteModes.Normal;
|
||||
}
|
||||
|
||||
private static string GetStatusPath(string mode) =>
|
||||
mode == SiteModes.UnderConstruction
|
||||
? "/under-construction.html"
|
||||
: "/site-unavailable.html";
|
||||
|
||||
private static bool IsAllowedPath(string path, string statusPath)
|
||||
{
|
||||
if (path.Equals(statusPath, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (path.Equals("/favicon.ico", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
return StaticAssetPrefixes.Any(prefix =>
|
||||
path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
// Program.cs
|
||||
using Web.Middleware;
|
||||
using Web.Settings;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddRouting();
|
||||
builder.Services.Configure<SiteSettings>(builder.Configuration.GetSection(SiteSettings.SectionName));
|
||||
|
||||
builder.Services.AddReverseProxy()
|
||||
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseMiddleware<SiteModeMiddleware>();
|
||||
|
||||
// Static site
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Web.Settings;
|
||||
|
||||
public sealed class SiteSettings
|
||||
{
|
||||
public const string SectionName = "Site";
|
||||
|
||||
/// <summary>
|
||||
/// Controls which page visitors see: Normal, UnderConstruction, or Unavailable.
|
||||
/// </summary>
|
||||
public string Mode { get; set; } = SiteModes.Normal;
|
||||
}
|
||||
|
||||
public static class SiteModes
|
||||
{
|
||||
public const string Normal = "Normal";
|
||||
public const string UnderConstruction = "UnderConstruction";
|
||||
public const string Unavailable = "Unavailable";
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Site": {
|
||||
"Mode": "Normal"
|
||||
},
|
||||
"ReverseProxy": {
|
||||
"Routes": {
|
||||
"apiRoute": {
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
|
||||
"Site": {
|
||||
"Mode": "Normal"
|
||||
},
|
||||
|
||||
"ReverseProxy": {
|
||||
"Routes": {
|
||||
"apiRoute": {
|
||||
|
||||
@@ -48,6 +48,33 @@ img {
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.status-hero {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 48px 0
|
||||
}
|
||||
|
||||
.status-card {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 40px;
|
||||
border-radius: var(--card-radius);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
box-shadow: var(--shadow)
|
||||
}
|
||||
|
||||
.status-brand {
|
||||
margin-bottom: 28px
|
||||
}
|
||||
|
||||
.status-note {
|
||||
margin: 24px 0 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.6
|
||||
}
|
||||
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MyAi.ro · Temporarily unavailable</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#071326" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="icon" type="image/png" href="/img/favicon-256.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/myai.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="site-shell">
|
||||
<main>
|
||||
<section class="hero status-hero">
|
||||
<div class="container">
|
||||
<div class="status-card">
|
||||
<a class="brand status-brand" href="/" aria-label="MyAi.ro home">
|
||||
<span class="brand-mark">
|
||||
<img src="/img/myai-logo.svg" alt="MyAi.ro">
|
||||
</span>
|
||||
<span>
|
||||
<span class="brand-text">MyAi.ro</span>
|
||||
<small>AI engineering showcase</small>
|
||||
</span>
|
||||
</a>
|
||||
<span class="eyebrow">Temporarily unavailable</span>
|
||||
<h1>The site is not available right now.</h1>
|
||||
<p class="hero-text">
|
||||
MyAi.ro is temporarily offline for maintenance.
|
||||
We expect to be back shortly.
|
||||
</p>
|
||||
<p class="status-note">
|
||||
<strong>RO:</strong> Site-ul nu este disponibil temporar. Vă mulțumim pentru răbdare.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MyAi.ro · Under construction</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#071326" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="icon" type="image/png" href="/img/favicon-256.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/myai.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="site-shell">
|
||||
<main>
|
||||
<section class="hero status-hero">
|
||||
<div class="container">
|
||||
<div class="status-card">
|
||||
<a class="brand status-brand" href="/" aria-label="MyAi.ro home">
|
||||
<span class="brand-mark">
|
||||
<img src="/img/myai-logo.svg" alt="MyAi.ro">
|
||||
</span>
|
||||
<span>
|
||||
<span class="brand-text">MyAi.ro</span>
|
||||
<small>AI engineering showcase</small>
|
||||
</span>
|
||||
</a>
|
||||
<span class="eyebrow">Under construction</span>
|
||||
<h1>We are building something new.</h1>
|
||||
<p class="hero-text">
|
||||
MyAi.ro is being updated with new AI demos and improvements.
|
||||
Please check back soon.
|
||||
</p>
|
||||
<p class="status-note">
|
||||
<strong>RO:</strong> Site-ul este în construcție. Reveniți în curând.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user