424 lines
14 KiB
C#
424 lines
14 KiB
C#
namespace MyOffice.Web;
|
|
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Core.Extensions;
|
|
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.Extensions.Hosting;
|
|
using Microsoft.Extensions.Options;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Identity;
|
|
using Identity.Domain;
|
|
using Identity.ExternalProviders;
|
|
using Identity.Repositories;
|
|
using IdentityServer4.AccessTokenValidation;
|
|
using IdentityServer4.Configuration;
|
|
using IdentityServer4.Extensions;
|
|
using IdentityServer4.Models;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Core.Identity;
|
|
using Data.Repositories.Account;
|
|
using Data.Repositories.Currency;
|
|
using Data.Repositories.Item;
|
|
using Data.Repositories.Users;
|
|
using DbContext;
|
|
using Services.Users;
|
|
using Shared;
|
|
using Identity.Configure;
|
|
using Infrastructure;
|
|
using Microsoft.Extensions.Logging.Console;
|
|
using IdentityServer4.Services;
|
|
using Models.Account;
|
|
using MyOffice.Services.Currency;
|
|
using Services.Account;
|
|
using Services.Item;
|
|
using MyOffice.Services.Dashboard;
|
|
using Services.Account.Domain;
|
|
using MyOffice.Services.Identity;
|
|
using AutoMapper;
|
|
|
|
public class Program
|
|
{
|
|
public static void Main(string[] args)
|
|
{
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
AddServices(builder);
|
|
|
|
var app = builder.Build();
|
|
Configure(app);
|
|
|
|
app.Run();
|
|
}
|
|
|
|
private static void AddServices(WebApplicationBuilder builder)
|
|
{
|
|
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"));
|
|
|
|
// shared configuration
|
|
builder.Configuration.AddConfiguration(SharedConfiguration.CreateConfigurationContainer());
|
|
|
|
// 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);
|
|
|
|
// Configurations
|
|
builder.Services.Configure<ExternalProvidersConfig>(
|
|
builder.Configuration.GetSection("ExternalProviders")
|
|
);
|
|
|
|
InitializeGlobalSettings(builder);
|
|
|
|
AddIdentityServices(builder);
|
|
|
|
builder.Services.AddScoped<IContextProvider, ContextProvider>();
|
|
|
|
AddMapping(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;
|
|
});
|
|
|
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
|
//builder.Services.AddEndpointsApiExplorer();
|
|
//builder.Services.AddSwaggerGen();
|
|
|
|
builder.Services.AddCors();
|
|
}
|
|
|
|
private static void InitializeGlobalSettings(WebApplicationBuilder builder)
|
|
{
|
|
builder.Services.Configure<GlobalSettings>(_ => { });
|
|
builder.Services.AddSingleton(resolver => resolver.GetRequiredService<IOptions<GlobalSettings>>().Value);
|
|
}
|
|
|
|
private static void AddIdentityServices(WebApplicationBuilder builder)
|
|
{
|
|
// add identity
|
|
builder.Services
|
|
.AddIdentity<ApplicationUser<Guid>, ApplicationRole>()
|
|
.AddUserStore<UserStore>()
|
|
.AddRoleStore<RoleStore>()
|
|
.AddUserManager<AppUserManager>()
|
|
.AddDefaultTokenProviders();
|
|
|
|
builder.Services.AddScoped<ICorsPolicyService, CorsPolicyService>();
|
|
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;
|
|
|
|
// 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;
|
|
});
|
|
|
|
// Adds IdentityServer.
|
|
builder.Services
|
|
.AddIdentityServer(o =>
|
|
{
|
|
o.UserInteraction = new UserInteractionOptions()
|
|
{
|
|
LoginUrl = "/authentication/signin",
|
|
LoginReturnUrlParameter = "returnUrl"
|
|
};
|
|
//o.IssuerUri = "http://localhost:9300";
|
|
})
|
|
.AddSigningCredential(CreateSigningCredential(builder))
|
|
.AddPersistedGrantStore<PersistedGrantStore>()
|
|
.AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
|
|
.AddInMemoryApiScopes(IdentityServerConfig.GetApiScopes())
|
|
.AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
|
|
.AddInMemoryClients(IdentityServerConfig.GetClients())
|
|
.AddAspNetIdentity<ApplicationUser<Guid>>()
|
|
.AddProfileService<ProfileService>()
|
|
.AddExtensionGrantValidator<ExtensionGrantValidator>();
|
|
|
|
var authentication = builder.Services
|
|
.AddAuthentication(options =>
|
|
{
|
|
options.DefaultScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
|
|
options.DefaultAuthenticateScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
|
|
options.DefaultChallengeScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
|
|
});
|
|
|
|
var frontEndHost = builder.Configuration.GetValue<string>("FrontEnd:Host");
|
|
var isDynamicFrontEndHost = frontEndHost.IsMissing();
|
|
if (isDynamicFrontEndHost)
|
|
{
|
|
builder.Services.ConfigureOptions<ConfigureIdentityServerOptions>();
|
|
authentication.AddIdentityServerAuthentication();
|
|
}
|
|
else
|
|
{
|
|
authentication.AddIdentityServerAuthentication(options =>
|
|
{
|
|
options.RequireHttpsMetadata = false;
|
|
options.ApiName = IdentityServerConfig.ApiName;
|
|
options.Authority = frontEndHost;
|
|
});
|
|
}
|
|
}
|
|
|
|
private static void AddMapping(WebApplicationBuilder builder)
|
|
{
|
|
builder.Services.AddSingleton(provider => new MapperConfiguration(cfg =>
|
|
{
|
|
cfg.AddProfile(new AccountMappingProfile());
|
|
cfg.AddProfile(new ViewModelProfile());
|
|
cfg.AddProfile(new AccountViewModelProfile(provider.CreateScope().ServiceProvider.GetService<IContextProvider>()!));
|
|
}).CreateMapper());
|
|
|
|
//builder.Services.AddAutoMapper(typeof(AccountMappingProfile));
|
|
//builder.Services.AddAutoMapper(typeof(AccountViewModelProfile));
|
|
//builder.Services.AddAutoMapper(typeof(ViewModelProfile));
|
|
}
|
|
|
|
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<IAccountAccountCategoryRepository, AccountAccountCategoryRepository>();
|
|
|
|
builder.Services.AddScoped<IItemCategoryRepository, ItemCategoryRepository>();
|
|
builder.Services.AddScoped<IItemRepository, ItemRepository>();
|
|
builder.Services.AddScoped<IItemGlobalRepository, ItemGlobalRepository>();
|
|
builder.Services.AddScoped<IMotionRepository, MotionRepository>();
|
|
}
|
|
|
|
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 bool _firstRequest = true;
|
|
private static readonly object LockFirstRequest = new();
|
|
|
|
private static void Configure(WebApplication app)
|
|
{
|
|
// Configure the HTTP request pipeline.
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseDeveloperExceptionPage();
|
|
IdentityModelEventSource.ShowPII = true;
|
|
}
|
|
else
|
|
{
|
|
IdentityModelEventSource.ShowPII = true;
|
|
app.UseExceptionHandler("/Error");
|
|
}
|
|
|
|
app.UseDefaultFiles();
|
|
app.UseStaticFiles();
|
|
|
|
var logger = app.Logger;
|
|
|
|
// configuration on first request
|
|
app.Use(async (ctx, next) =>
|
|
{
|
|
var path = ctx.Request.Path;
|
|
|
|
GlobalSettings? globalSettings = null;
|
|
|
|
if (_firstRequest)
|
|
{
|
|
// lock
|
|
lock (LockFirstRequest)
|
|
{
|
|
// recheck
|
|
if (_firstRequest)
|
|
{
|
|
_firstRequest = false;
|
|
|
|
globalSettings = ctx.RequestServices.GetRequiredService<GlobalSettings>();
|
|
|
|
var configuration = ctx.RequestServices.GetRequiredService<IConfiguration>();
|
|
var frontEndHost = configuration.GetValue<string>("FrontEnd:Host");
|
|
var isDynamicFrontEndHost = frontEndHost.IsMissing();
|
|
if (!isDynamicFrontEndHost)
|
|
{
|
|
globalSettings.Host = frontEndHost;
|
|
}
|
|
else
|
|
{
|
|
// set real frontend host
|
|
globalSettings.Host = GetFrontendHost(ctx, logger);
|
|
}
|
|
|
|
// update identity server
|
|
var options = ctx.RequestServices.GetRequiredService<IdentityServerOptions>();
|
|
options.IssuerUri = globalSettings.Host;
|
|
|
|
// update identity server client redirect uri
|
|
var clients = ctx.RequestServices.GetRequiredService<IEnumerable<Client>>();
|
|
var client = clients.FirstOrDefault()!;
|
|
client.RedirectUris = new List<string>
|
|
{
|
|
$"{globalSettings.Host}/silent-refresh.html"
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
if (path.Value.EqualsIgnoreCase("/.well-known/openid-configuration"))
|
|
{
|
|
globalSettings ??= ctx.RequestServices.GetRequiredService<GlobalSettings>();
|
|
logger.LogInformation($"OC Host: {globalSettings.Host}");
|
|
ctx.SetIdentityServerOrigin(globalSettings.Host);
|
|
var options = ctx.RequestServices.GetRequiredService<IdentityServerOptions>();
|
|
options.IssuerUri = globalSettings.Host;
|
|
}
|
|
|
|
await next();
|
|
});
|
|
|
|
// send index.html to any routes except coreRoutes
|
|
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) return;
|
|
if (coreRoutes.Any(x => path.StartsWithSegments(x, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
await next();
|
|
}
|
|
else
|
|
{
|
|
if (!_staticFilesCache.TryGetValue(path.Value, out var physicalFileInfo))
|
|
{
|
|
var segments = path.Value.Split('/', '\\');
|
|
var fileInfo = new FileInfo(Path.Combine(wwwrootPath, segments.LastOrDefault()!));
|
|
if (!fileInfo.Exists)
|
|
{
|
|
return;
|
|
}
|
|
|
|
physicalFileInfo = new PhysicalFileInfo(fileInfo);
|
|
_staticFilesCache.TryAdd(path.Value, physicalFileInfo);
|
|
}
|
|
|
|
await context.Response.SendFileAsync(physicalFileInfo);
|
|
await context.Response.CompleteAsync();
|
|
}
|
|
});
|
|
|
|
app.UseCors(builder => builder
|
|
.AllowAnyOrigin()
|
|
.AllowAnyHeader()
|
|
.AllowAnyMethod());
|
|
|
|
app.UseIdentityServer();
|
|
app.UseAuthorization();
|
|
|
|
app.MapControllers();
|
|
}
|
|
|
|
private static string GetFrontendHost(HttpContext ctx, ILogger logger)
|
|
{
|
|
// internet <-> nginx (https) <-> site(http)
|
|
var host = $"{ctx.Request.Headers["Referer"]}";
|
|
host = host.EndsWith("/") ? host.Substring(0, host.Length - 1) : host;
|
|
|
|
return host;
|
|
}
|
|
|
|
private static SigningCredentials CreateSigningCredential(WebApplicationBuilder builder)
|
|
{
|
|
var rsa = RSA.Create();
|
|
|
|
var webRootPath = builder.Configuration.GetValue<string>(WebHostDefaults.ContentRootKey);
|
|
var privateKey = File.ReadAllText(Path.Combine(webRootPath, "rsa_key.pem"));
|
|
privateKey = privateKey
|
|
.Replace("-----BEGIN RSA PRIVATE KEY-----", string.Empty)
|
|
.Replace("-----END RSA PRIVATE KEY-----", string.Empty)
|
|
.Replace(Environment.NewLine, string.Empty);
|
|
|
|
var privateKeyBytes = Convert.FromBase64String(privateKey);
|
|
rsa.ImportRSAPrivateKey(privateKeyBytes, out int _);
|
|
|
|
var rsaSecurityKey = new RsaSecurityKey(rsa);
|
|
var credentials = new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha256);
|
|
|
|
return credentials;
|
|
}
|
|
} |