sync public allowlist from private myoffice
This commit is contained in:
@@ -1,449 +0,0 @@
|
||||
namespace MyOffice.Web;
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.FileProviders.Physical;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using Identity;
|
||||
using Identity.Domain;
|
||||
using Identity.ExternalProviders;
|
||||
using Identity.Repositories;
|
||||
using Auth;
|
||||
using Core.Identity;
|
||||
using Core.Extensions;
|
||||
using Data.Repositories.Account;
|
||||
using Data.Repositories.Currency;
|
||||
using Data.Repositories.Item;
|
||||
using Data.Repositories.Users;
|
||||
using DbContext;
|
||||
using Services.Users;
|
||||
using Shared;
|
||||
using Infrastructure;
|
||||
using Models.Account;
|
||||
using MyOffice.Services.Currency;
|
||||
using Services.Account;
|
||||
using Services.Item;
|
||||
using MyOffice.Services.Dashboard;
|
||||
using MyOffice.Services.Identity;
|
||||
using MyOffice.Services.Account.Domain;
|
||||
using MyOffice.Web.Infrastructure.Attributes;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using MyOffice.Web.Infrastructure.Filters;
|
||||
|
||||
//TODO: Data.Model only Repository and Service, response <-> mapper <-> web <-> mapper <-> service <-> repository
|
||||
//TODO: Project management
|
||||
//TODO: Model names. Controller = xxxRequest/xxxResponse. Service xxxInput/xxxOutput.
|
||||
//TODO: Validate string as Guid when id
|
||||
//TODO: Test back
|
||||
//TODO: Test front
|
||||
//TODO: Test DB ???
|
||||
//TODO: Email confirmation
|
||||
//TODO: Password recovery
|
||||
//TODO: Account sharing
|
||||
//TODO: /dashboard/dashboard -> /dashboard/main or /dashboard
|
||||
//TODO: SPA load categories twice menu + setting (cache)
|
||||
//TODO: SPA update account category -> update menu
|
||||
//TODO: Response model from base type
|
||||
//TODO: XXXResult -> XXXStatus
|
||||
//TODO: SPA isAllowDelete -> allowDelete (remove is)
|
||||
//TODO: Repository auto registration
|
||||
//TODO: Services auto registration
|
||||
//TODO: openid-configuration failed - lock login
|
||||
//TODO: BUG some times after login redirect to dashboard but exists return url
|
||||
//TODO: automapper -> Extension ToModel() ToDbo() FromModel() FromDbo()
|
||||
//TODO: SPA all http requests -> services
|
||||
//TODO: Items, select category -> save url to allow refresh
|
||||
//TODO: Accounts, select category -> save url to allow refresh
|
||||
|
||||
public partial class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
AddServices(builder, args);
|
||||
|
||||
var app = builder.Build();
|
||||
Configure(app);
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
||||
private static void AddServices(WebApplicationBuilder builder, string[] args)
|
||||
{
|
||||
#region Loging
|
||||
|
||||
builder.Services.AddLogging(logging =>
|
||||
logging.AddSimpleConsole(options =>
|
||||
{
|
||||
//options.SingleLine = true;
|
||||
options.TimestampFormat = "[HH:mm:ss] ";
|
||||
options.ColorBehavior = LoggerColorBehavior.Enabled;
|
||||
})
|
||||
);
|
||||
//File Logger
|
||||
builder.Logging.AddFile(builder.Configuration.GetSection("Logging"));
|
||||
|
||||
#endregion Loging
|
||||
|
||||
builder.Services.Configure<LoggerFilterOptions>(options =>
|
||||
{
|
||||
options.AddFilter("OpenIddict", LogLevel.Warning);
|
||||
});
|
||||
|
||||
// Shared JSON after appsettings.*, then restore higher-priority sources
|
||||
builder.Configuration.AddSharedAppSettings(builder.Environment.EnvironmentName);
|
||||
builder.Configuration.AddEnvironmentVariables();
|
||||
builder.Configuration.AddCommandLine(args);
|
||||
|
||||
#region Database
|
||||
|
||||
var databaseProvider = builder.Configuration["DatabaseProvider"];
|
||||
if (databaseProvider == null)
|
||||
throw new NullReferenceException($"Configuration DatabaseProvider {databaseProvider}");
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString(databaseProvider);
|
||||
if (connectionString == null)
|
||||
throw new NullReferenceException($"Configuration ConnectionString {databaseProvider}");
|
||||
|
||||
var connectionConfiguration = new ConnectionConfiguration(databaseProvider, connectionString);
|
||||
RepositoryInitializer.Initialize(builder.Services, connectionConfiguration);
|
||||
// Before OpenIddictSeeder so the schema exists when clients/scopes are registered.
|
||||
builder.Services.AddHostedService<DatabaseInitializerHostedService>();
|
||||
|
||||
#endregion Database
|
||||
|
||||
// Configurations
|
||||
builder.Services.Configure<ExternalProvidersConfig>(
|
||||
builder.Configuration.GetSection("ExternalProviders")
|
||||
);
|
||||
|
||||
InitializeGlobalSettings(builder);
|
||||
|
||||
AddIdentityServices(builder);
|
||||
|
||||
builder.Services.AddScoped<IContextProvider, ContextProvider>();
|
||||
|
||||
AddAutoMapper(builder);
|
||||
|
||||
AddRepositories(builder);
|
||||
|
||||
AddBusinessServices(builder);
|
||||
|
||||
builder.Services
|
||||
.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.AllowInputFormatterExceptionMessages = builder.Environment.IsDevelopment();
|
||||
options.JsonSerializerOptions.MaxDepth = 0;
|
||||
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
|
||||
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
|
||||
});
|
||||
|
||||
builder.Services.AddCors();
|
||||
|
||||
builder.Services.AddControllersWithViews(options =>
|
||||
{
|
||||
options.Conventions.Add(new DefaultFromBodyBindingConvention());
|
||||
options.Filters.Add(typeof(GlobalModelStateValidatorAttribute));
|
||||
options.Filters.Add(typeof(ResponseFilter));
|
||||
});
|
||||
|
||||
builder.Services.Configure<MvcOptions>(options =>
|
||||
{
|
||||
});
|
||||
|
||||
builder.Services.Configure<ApiBehaviorOptions>(x => {
|
||||
});
|
||||
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddExceptionHandler<UnhandledExceptionHandler>();
|
||||
builder.Services.AddSingleton<ProblemDetailsFactory, CustomProblemDetailsFactory>();
|
||||
}
|
||||
|
||||
private static void InitializeGlobalSettings(WebApplicationBuilder builder)
|
||||
{
|
||||
var frontEndHost = builder.Configuration.GetValue<string>("FrontEnd:Host");
|
||||
if (frontEndHost.IsMissing())
|
||||
{
|
||||
frontEndHost = "http://localhost:4300";
|
||||
if (!builder.Environment.IsDevelopment())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"FrontEnd:Host must be set outside Development (do not derive it from Referer).");
|
||||
}
|
||||
}
|
||||
|
||||
frontEndHost = frontEndHost!.TrimEnd('/');
|
||||
builder.Services.Configure<GlobalSettings>(options => options.Host = frontEndHost);
|
||||
builder.Services.AddSingleton(resolver => resolver.GetRequiredService<IOptions<GlobalSettings>>().Value);
|
||||
}
|
||||
|
||||
private static void AddIdentityServices(WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
// add identity
|
||||
builder.Services
|
||||
.AddIdentity<ApplicationUser<Guid>, ApplicationRole>()
|
||||
.AddUserStore<UserStore>()
|
||||
.AddRoleStore<RoleStore>()
|
||||
.AddUserManager<AppUserManager>()
|
||||
.AddSignInManager<SignInManager<ApplicationUser<Guid>>>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
builder.Services.AddScoped<IPasswordHasher<ApplicationUser<Guid>>, PasswordHasher>();
|
||||
|
||||
// Identity Services
|
||||
builder.Services.AddScoped<IUserStore<ApplicationUser<Guid>>, UserStore>();
|
||||
builder.Services.AddScoped<IRoleStore<ApplicationRole>, RoleStore>();
|
||||
|
||||
// External providers
|
||||
builder.Services.AddScoped<IExternalProviderValidator, ExternalProviderValidatorAuth0>();
|
||||
builder.Services.AddScoped<IExternalProviderValidator, ExternalProviderValidatorGoogle>();
|
||||
|
||||
// Configure Identity options and password complexity here
|
||||
builder.Services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
// User settings
|
||||
options.User.RequireUniqueEmail = true;
|
||||
|
||||
// Email+password accounts can sign in immediately after register
|
||||
options.SignIn.RequireConfirmedAccount = false;
|
||||
options.SignIn.RequireConfirmedEmail = false;
|
||||
|
||||
// Password settings
|
||||
options.Password.RequireDigit = true;
|
||||
options.Password.RequiredLength = 8;
|
||||
options.Password.RequireNonAlphanumeric = true;
|
||||
options.Password.RequireUppercase = true;
|
||||
options.Password.RequireLowercase = true;
|
||||
|
||||
// Lockout settings
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
|
||||
options.Lockout.MaxFailedAccessAttempts = 10;
|
||||
});
|
||||
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders =
|
||||
ForwardedHeaders.XForwardedFor
|
||||
| ForwardedHeaders.XForwardedProto
|
||||
| ForwardedHeaders.XForwardedHost;
|
||||
// Trust nginx in Docker; headers are only present behind the proxy.
|
||||
options.KnownNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
});
|
||||
|
||||
builder.Services.AddMyOfficeOpenIddict(builder.Configuration, builder.Environment);
|
||||
}
|
||||
|
||||
private static void AddAutoMapper(WebApplicationBuilder builder)
|
||||
{
|
||||
// AutoMapper 15+ dual license: runs without a key (warning logs only).
|
||||
// Set AutoMapper:LicenseKey (or AUTOMAPPER_LICENSE_KEY) from https://automapper.io — free Community tier if under $5M revenue.
|
||||
var licenseKey = builder.Configuration["AutoMapper:LicenseKey"];
|
||||
|
||||
builder.Services.AddAutoMapper(
|
||||
c =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(licenseKey))
|
||||
{
|
||||
c.LicenseKey = licenseKey;
|
||||
}
|
||||
|
||||
c.AllowNullCollections = true;
|
||||
c.AllowNullDestinationValues = true;
|
||||
},
|
||||
typeof(AccountDto).Assembly,
|
||||
typeof(AccountViewModel).Assembly
|
||||
);
|
||||
}
|
||||
|
||||
private static void AddRepositories(WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
||||
builder.Services.AddScoped<IUserExternalRepository, UserExternalRepository>();
|
||||
|
||||
builder.Services.AddScoped<ICurrencyGlobalRepository, CurrencyGlobalRepository>();
|
||||
builder.Services.AddScoped<ICurrencyRepository, CurrencyRepository>();
|
||||
builder.Services.AddScoped<ICurrencyRateRepository, CurrencyRateRepository>();
|
||||
|
||||
builder.Services.AddScoped<IAccountCategoryRepository, AccountCategoryRepository>();
|
||||
builder.Services.AddScoped<IAccountRepository, AccountRepository>();
|
||||
builder.Services.AddScoped<IAccountAccessRepository, AccountAccessRepository>();
|
||||
builder.Services.AddScoped<IAccountAccessInviteRepository, AccountAccessInviteRepository>();
|
||||
builder.Services.AddScoped<IAccountAccountCategoryRepository, AccountAccountCategoryRepository>();
|
||||
|
||||
builder.Services.AddScoped<IItemCategoryRepository, ItemCategoryRepository>();
|
||||
builder.Services.AddScoped<IItemRepository, ItemRepository>();
|
||||
builder.Services.AddScoped<IItemGlobalRepository, ItemGlobalRepository>();
|
||||
builder.Services.AddScoped<IMotionRepository, MotionRepository>();
|
||||
|
||||
builder.Services.AddScoped<IVerificationCodeRepository, VerificationCodeRepository>();
|
||||
builder.Services.AddScoped<IEmailTemplateRepository, EmailTemplateRepository>();
|
||||
}
|
||||
|
||||
private static void AddBusinessServices(WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddScoped<UserService, UserService>();
|
||||
builder.Services.AddScoped<CurrencyService, CurrencyService>();
|
||||
builder.Services.AddScoped<AccountService, AccountService>();
|
||||
builder.Services.AddScoped<ItemService, ItemService>();
|
||||
builder.Services.AddScoped<DashboardService, DashboardService>();
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, PhysicalFileInfo> _staticFilesCache = new();
|
||||
|
||||
private static void Configure(WebApplication app)
|
||||
{
|
||||
// nginx (prod) terminates TLS and forwards X-Forwarded-Proto/Host
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler();
|
||||
app.UseStatusCodePages();
|
||||
}
|
||||
|
||||
// Explicit routing so CORS runs before endpoint matching (preflight / OPTIONS).
|
||||
app.UseRouting();
|
||||
ConfigureCors(app);
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
var logger = app.Logger;
|
||||
var globalSettings = app.Services.GetRequiredService<GlobalSettings>();
|
||||
logger.LogInformation("FrontEnd host: {Host}", globalSettings.Host);
|
||||
|
||||
// send index.html / static files for non-API routes
|
||||
var coreRoutes = new[]
|
||||
{
|
||||
// API routes
|
||||
new PathString("/api"),
|
||||
// Identity server routes
|
||||
new PathString("/.well-known"),
|
||||
new PathString("/connect"),
|
||||
new PathString("/silent-refresh.html")
|
||||
};
|
||||
|
||||
var webRootPath = app.Configuration.GetValue<string>(WebHostDefaults.ContentRootKey);
|
||||
var wwwrootPath = Path.Combine(webRootPath!, "wwwroot");
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var path = context.Request.Path;
|
||||
|
||||
if (path.Value == null)
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (coreRoutes.Any(x => path.StartsWithSegments(x, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_staticFilesCache.TryGetValue(path.Value, out var physicalFileInfo))
|
||||
{
|
||||
var segments = path.Value.Split('/', '\\');
|
||||
var fileName = segments.LastOrDefault();
|
||||
var fileInfo = string.IsNullOrEmpty(fileName)
|
||||
? null
|
||||
: new FileInfo(Path.Combine(wwwrootPath, fileName));
|
||||
|
||||
if (fileInfo is null || !fileInfo.Exists)
|
||||
{
|
||||
// SPA deep link fallback
|
||||
var indexInfo = new FileInfo(Path.Combine(wwwrootPath, "index.html"));
|
||||
if (!indexInfo.Exists)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
physicalFileInfo = new PhysicalFileInfo(indexInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
physicalFileInfo = new PhysicalFileInfo(fileInfo);
|
||||
_staticFilesCache.TryAdd(path.Value, physicalFileInfo);
|
||||
}
|
||||
}
|
||||
|
||||
await context.Response.SendFileAsync(physicalFileInfo);
|
||||
});
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
MapRoutes(app);
|
||||
|
||||
app.MapControllers();
|
||||
}
|
||||
|
||||
private static void ConfigureCors(WebApplication app)
|
||||
{
|
||||
var allowedOrigins = (app.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [])
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(x => x.Trim().TrimEnd('/'))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var frontEndHost = app.Configuration.GetValue<string>("FrontEnd:Host")?.Trim().TrimEnd('/');
|
||||
if (!string.IsNullOrWhiteSpace(frontEndHost)
|
||||
&& !allowedOrigins.Contains(frontEndHost, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
allowedOrigins.Add(frontEndHost);
|
||||
}
|
||||
|
||||
var isDocker = string.Equals(
|
||||
app.Environment.EnvironmentName,
|
||||
"Docker",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
app.Logger.LogInformation(
|
||||
"CORS origins: {Origins}",
|
||||
allowedOrigins.Count > 0 ? string.Join(", ", allowedOrigins) : "(none)");
|
||||
|
||||
app.UseCors(policy =>
|
||||
{
|
||||
if (allowedOrigins.Count > 0)
|
||||
{
|
||||
policy.WithOrigins(allowedOrigins.ToArray())
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
}
|
||||
else if (app.Environment.IsDevelopment() || isDocker)
|
||||
{
|
||||
// Demo / local: SPA and API are on different host ports.
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user