diff --git a/Aberwyn/Aberwyn.csproj b/Aberwyn/Aberwyn.csproj index 92a479f..cc74855 100644 --- a/Aberwyn/Aberwyn.csproj +++ b/Aberwyn/Aberwyn.csproj @@ -9,6 +9,13 @@ Linux + + + + + + + @@ -20,6 +27,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -42,7 +50,6 @@ - diff --git a/Aberwyn/Controllers/SetupApiController.cs b/Aberwyn/Controllers/SetupApiController.cs new file mode 100644 index 0000000..5240a56 --- /dev/null +++ b/Aberwyn/Controllers/SetupApiController.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Mvc; +using MySql.Data.MySqlClient; + +namespace Aberwyn.Controllers +{ + [ApiController] + [Route("api/setup")] + public class SetupApiController : ControllerBase + { + [HttpPost("testdb")] + public IActionResult TestDbConnection([FromBody] DbTestRequest request) + { + try + { + var baseConnStr = $"server={request.Host};port={request.Port};user={request.User};password={request.Pass};"; + + var testDbName = $"testcheck_{Guid.NewGuid():N}".Substring(0, 12); // säker tillfällig databas + + using (var conn = new MySqlConnection(baseConnStr + "database=information_schema;")) + { + conn.Open(); + + // Försök skapa en temporär databas + var createCmd = new MySqlCommand($"CREATE DATABASE `{testDbName}`", conn); + createCmd.ExecuteNonQuery(); + + // Och radera den direkt + var dropCmd = new MySqlCommand($"DROP DATABASE `{testDbName}`", conn); + dropCmd.ExecuteNonQuery(); + } + + return Ok(new { success = true, message = "Anslutning OK och CREATE DATABASE tillåten." }); + } + catch (Exception ex) + { + return Ok(new { success = false, message = ex.Message }); + } + } + + + public class DbTestRequest + { + public string Host { get; set; } + public string Port { get; set; } + public string Db { get; set; } + public string User { get; set; } + public string Pass { get; set; } + } + } +} diff --git a/Aberwyn/Controllers/SetupController.cs b/Aberwyn/Controllers/SetupController.cs new file mode 100644 index 0000000..e7d7f59 --- /dev/null +++ b/Aberwyn/Controllers/SetupController.cs @@ -0,0 +1,151 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; + +namespace Aberwyn.Controllers +{ + using System.Text.Json; + using System.IO; + using Aberwyn.Models; + using Microsoft.AspNetCore.Mvc.Filters; + using MySql.Data.MySqlClient; + using Aberwyn.Data; + using Microsoft.EntityFrameworkCore; + + [Route("setup")] + public class SetupController : Controller + { + private readonly IWebHostEnvironment _env; + + public override void OnActionExecuting(ActionExecutingContext context) + { + ViewBag.IsSetupMode = true; + base.OnActionExecuting(context); + } + public SetupController(IWebHostEnvironment env) + { + _env = env; + } + + [HttpGet] + public IActionResult Index() + { + return View(new SetupSettings()); + } + + [Authorize(Roles = "Admin")] + [HttpPost("reset")] + public IActionResult Reset() + { + var path = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json"); + + var resetSettings = new SetupSettings + { + IsConfigured = false, + DbHost = "", + DbPort = 3306, + DbName = "", + DbUser = "", + DbPassword = "", + AdminUsername = "admin", + AdminEmail = "admin@localhost", + AdminPassword = "Admin123!" + }; + + var json = JsonSerializer.Serialize(resetSettings, new JsonSerializerOptions { WriteIndented = true }); + System.IO.File.WriteAllText(path, json); + + return RedirectToAction("Index"); + } + + + [HttpPost("")] + public async Task Setup([FromBody] SetupSettings model) + { + if (!ModelState.IsValid) + { + var allErrors = ModelState + .Where(e => e.Value.Errors.Count > 0) + .Select(e => new { Field = e.Key, Errors = e.Value.Errors.Select(x => x.ErrorMessage) }); + + return BadRequest(new { error = "Modellen är ogiltig", details = allErrors }); + } + + try + { + // Skapa databasen om den inte finns + var baseConnStr = $"server={model.DbHost};port={model.DbPort};user={model.DbUser};password={model.DbPassword};"; + using (var conn = new MySqlConnection(baseConnStr + "database=information_schema;")) + { + conn.Open(); + var cmd = new MySqlCommand("SELECT SCHEMA_NAME FROM SCHEMATA WHERE SCHEMA_NAME = @dbName", conn); + cmd.Parameters.AddWithValue("@dbName", model.DbName); + var exists = cmd.ExecuteScalar(); + + if (exists == null) + { + var createCmd = new MySqlCommand($"CREATE DATABASE `{model.DbName}`", conn); + createCmd.ExecuteNonQuery(); + } + } + + // Bygg services temporärt för att skapa admin + var connectionString = $"server={model.DbHost};port={model.DbPort};database={model.DbName};user={model.DbUser};password={model.DbPassword}"; + var tempProvider = SetupService.BuildTemporaryServices(connectionString); + + using var scope = tempProvider.CreateScope(); + + // Skapa databastabeller via EF + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); + + var userManager = scope.ServiceProvider.GetRequiredService>(); + var roleManager = scope.ServiceProvider.GetRequiredService>(); + + // Skapa roller + string[] roles = { "Admin", "Chef", "Budget" }; + foreach (var role in roles) + { + if (!await roleManager.RoleExistsAsync(role)) + await roleManager.CreateAsync(new IdentityRole(role)); + } + + // Skapa adminanvändare + var adminUser = new ApplicationUser + { + UserName = model.AdminUsername, + Email = model.AdminEmail + }; + + var existingUser = await userManager.FindByNameAsync(model.AdminUsername); + if (existingUser == null) + { + var result = await userManager.CreateAsync(adminUser, model.AdminPassword); + if (!result.Succeeded) + { + return BadRequest(new { error = "Kunde inte skapa administratör", details = result.Errors }); + } + + await userManager.AddToRoleAsync(adminUser, "Admin"); + } + + model.IsConfigured = true; + + // Spara inställningarna + var json = JsonSerializer.Serialize(model, new JsonSerializerOptions { WriteIndented = true }); + var filePath = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json"); + System.IO.File.WriteAllText(filePath, json); + + return Ok(new { message = "Installation slutförd!" }); + } + catch (Exception ex) + { + return BadRequest(new { error = "Fel vid installation", details = ex.Message }); + } + } + + public IActionResult SetupComplete() => View(); + } + + +} diff --git a/Aberwyn/Data/IdentityDataInitializer.cs b/Aberwyn/Data/IdentityDataInitializer.cs index 35be21e..189f483 100644 --- a/Aberwyn/Data/IdentityDataInitializer.cs +++ b/Aberwyn/Data/IdentityDataInitializer.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Aberwyn.Models; @@ -6,10 +7,16 @@ namespace Aberwyn.Data { public static class IdentityDataInitializer { - public static async Task SeedData(IServiceProvider serviceProvider) + public static async Task SeedData(IServiceProvider serviceProvider, SetupSettings? setup = null) { var userManager = serviceProvider.GetRequiredService>(); var roleManager = serviceProvider.GetRequiredService>(); + var config = serviceProvider.GetService(); + + if (setup == null && config != null) + { + setup = config.GetSection("SetupSettings").Get() ?? new SetupSettings(); + } string[] roles = { "Admin", "Chef", "Budget" }; @@ -19,26 +26,38 @@ namespace Aberwyn.Data await roleManager.CreateAsync(new IdentityRole(role)); } - string adminUsername = "admin"; - string adminEmail = "admin@localhost"; - string password = "Admin123!"; - - if (await userManager.FindByEmailAsync(adminEmail) == null) + var existingUser = await userManager.FindByEmailAsync(setup.AdminEmail); + if (existingUser == null) { var user = new ApplicationUser { - UserName = adminUsername, - Email = adminEmail, + UserName = setup.AdminUsername, + Email = setup.AdminEmail, EmailConfirmed = true }; - var result = await userManager.CreateAsync(user, password); + var result = await userManager.CreateAsync(user, setup.AdminPassword); + + if (result.Succeeded) + await userManager.AddToRoleAsync(user, "Admin"); + + return result; + } + else + { + var token = await userManager.GeneratePasswordResetTokenAsync(existingUser); + var result = await userManager.ResetPasswordAsync(existingUser, token, setup.AdminPassword); if (result.Succeeded) { - await userManager.AddToRoleAsync(user, "Admin"); + var rolesForUser = await userManager.GetRolesAsync(existingUser); + if (!rolesForUser.Contains("Admin")) + await userManager.AddToRoleAsync(existingUser, "Admin"); } + + return result; } } + } } diff --git a/Aberwyn/Data/SetupService.cs b/Aberwyn/Data/SetupService.cs new file mode 100644 index 0000000..0166182 --- /dev/null +++ b/Aberwyn/Data/SetupService.cs @@ -0,0 +1,55 @@ +using Aberwyn.Models; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; + +namespace Aberwyn.Data +{ + // SetupService.cs + public class SetupService + { + private readonly IWebHostEnvironment _env; + private readonly string _filePath; + + public SetupService(IWebHostEnvironment env) + { + _env = env; + _filePath = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json"); + } + + + public SetupSettings GetSetup() + { + if (!File.Exists(_filePath)) + return new SetupSettings { IsConfigured = false }; + + var json = File.ReadAllText(_filePath); + return JsonSerializer.Deserialize(json) ?? new SetupSettings { IsConfigured = false }; + } + + + internal static IServiceProvider BuildTemporaryServices(string connectionString) + { + var services = new ServiceCollection(); + + // Konfigurera EF + Identity + services.AddDbContext(options => + options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); + + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + // Lägg till en tom konfiguration för att undvika null + services.AddSingleton(new ConfigurationBuilder().Build()); + + // Valfritt: Lägg till loggning om något kräver det + services.AddLogging(); + + return services.BuildServiceProvider(); + } + + } + +} diff --git a/Aberwyn/Infrastructure/setup.json b/Aberwyn/Infrastructure/setup.json new file mode 100644 index 0000000..f9402c6 --- /dev/null +++ b/Aberwyn/Infrastructure/setup.json @@ -0,0 +1,11 @@ +{ + "AdminUsername": "tai", + "AdminEmail": "tai@zcz.se", + "AdminPassword": "Admin123!", + "IsConfigured": true, + "DbHost": "localhost", + "DbPort": 3306, + "DbName": "aberwyn_test", + "DbUser": "root", + "DbPassword": "rootpass" +} \ No newline at end of file diff --git a/Aberwyn/Models/SetupSettings.cs b/Aberwyn/Models/SetupSettings.cs new file mode 100644 index 0000000..4b1f814 --- /dev/null +++ b/Aberwyn/Models/SetupSettings.cs @@ -0,0 +1,17 @@ +namespace Aberwyn.Models +{ + public class SetupSettings + { + public string AdminUsername { get; set; } = "admin"; + public string AdminEmail { get; set; } = "admin@localhost"; + public string AdminPassword { get; set; } = "Admin123!"; + public bool IsConfigured { get; set; } + public string DbHost { get; set; } + public int DbPort { get; set; } + public string DbName { get; set; } + public string DbUser { get; set; } + public string DbPassword { get; set; } + } + + +} diff --git a/Aberwyn/Program.cs b/Aberwyn/Program.cs index 19d3574..edcccf6 100644 --- a/Aberwyn/Program.cs +++ b/Aberwyn/Program.cs @@ -1,14 +1,12 @@ using Microsoft.EntityFrameworkCore; + using Aberwyn.Data; using System.Text; using System.Globalization; using Microsoft.AspNetCore.Localization; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Aberwyn.Models; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using System.Text.Json; var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) @@ -16,7 +14,6 @@ var config = new ConfigurationBuilder() .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true) .AddEnvironmentVariables() .Build(); -Console.WriteLine("📦 DEBUG: laddad raw-connectionstring: " + config.GetConnectionString("DefaultConnection")); var builder = WebApplication.CreateBuilder(new WebApplicationOptions { @@ -24,22 +21,47 @@ var builder = WebApplication.CreateBuilder(new WebApplicationOptions EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production" }); builder.Configuration.AddConfiguration(config); -Console.WriteLine("🌍 Miljö: " + builder.Environment.EnvironmentName); -Console.WriteLine("🔗 Connection string: " + builder.Configuration.GetConnectionString("DefaultConnection")); + +// Läser setup.json +var setupFilePath = Path.Combine("infrastructure", "setup.json"); +SetupSettings setup; +try +{ + if (!File.Exists(setupFilePath)) + throw new Exception("setup.json saknas."); + + using var jsonStream = File.OpenRead(setupFilePath); + setup = JsonSerializer.Deserialize(jsonStream)!; + + if (setup.IsConfigured) + { + if (string.IsNullOrWhiteSpace(setup.DbHost) || + string.IsNullOrWhiteSpace(setup.DbName) || + string.IsNullOrWhiteSpace(setup.DbUser) || + string.IsNullOrWhiteSpace(setup.DbPassword)) + { + throw new Exception("Databasinställningarna är ofullständiga."); + } + } +} +catch (Exception ex) +{ + Console.WriteLine($"❌ Fel vid läsning av setup.json: {ex.Message}"); + setup = new SetupSettings { IsConfigured = false }; +} -// Add services to the container. +// Add services to the container builder.Services.AddControllersWithViews() .AddJsonOptions(opts => { - // Beh�ll propertynamn som i C#-klassen (PascalCase) opts.JsonSerializerOptions.PropertyNamingPolicy = null; - // Ignorera null-v�rden vid serialisering opts.JsonSerializerOptions.IgnoreNullValues = true; }); + builder.Services.AddSession(options => { - options.IdleTimeout = TimeSpan.FromHours(1); + options.IdleTimeout = TimeSpan.FromDays(30); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); @@ -47,24 +69,32 @@ builder.Services.AddSession(options => builder.Services.AddRazorPages(); builder.Services.AddHttpClient(); -// Configure your DbContext with MySQLs -Console.WriteLine("🔗 Connection string: " + builder.Configuration.GetConnectionString("DefaultConnection")); - -builder.Services.AddDbContext(options => - options.UseMySql( - builder.Configuration.GetConnectionString("DefaultConnection"), - new MySqlServerVersion(new Version(8, 0, 36)), - mySqlOptions => mySqlOptions.EnableRetryOnFailure() - ) -); - - -builder.Services.AddIdentity(options => +// Registrera rätt databas och identity beroende på om setup är klar +if (setup.IsConfigured) { - options.SignIn.RequireConfirmedAccount = false; -}) -.AddEntityFrameworkStores() -.AddDefaultTokenProviders(); + string connectionString = $"Server={setup.DbHost};Port={setup.DbPort};Database={setup.DbName};User={setup.DbUser};Password={setup.DbPassword};"; + + builder.Services.AddDbContext(options => + options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); + + builder.Services.AddIdentity(options => + { + options.SignIn.RequireConfirmedAccount = false; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); +} +else +{ + builder.Services.AddDbContext(options => + options.UseInMemoryDatabase("TempSetup")); + + builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); +} + +// Identity inställningar builder.Services.Configure(options => { options.Password.RequireDigit = false; @@ -74,8 +104,7 @@ builder.Services.Configure(options => options.Password.RequireUppercase = false; }); -builder.Services.AddControllersWithViews(); -// Register your services +// Appens övriga tjänster builder.Services.AddScoped(); builder.Services.AddSingleton(sp => @@ -89,11 +118,14 @@ builder.Services.AddSingleton(sp => }); builder.Services.Configure(builder.Configuration.GetSection("Vapid")); + builder.Services.ConfigureApplicationCookie(options => { - options.LoginPath = "/Identity/Account/Login"; // korrekt för ditt nuvarande upplägg + options.LoginPath = "/Identity/Account/Login"; }); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + builder.Services.Configure(options => { var supportedCultures = new[] { new CultureInfo("sv-SE") }; @@ -101,60 +133,70 @@ builder.Services.Configure(options => options.SupportedCultures = supportedCultures; options.SupportedUICultures = supportedCultures; }); - -//builder.Configuration -// .SetBasePath(Directory.GetCurrentDirectory()) -// .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true) -// .AddEnvironmentVariables(); -builder.Services.AddSingleton(sp => -{ - var config = sp.GetRequiredService(); - return new PushNotificationService( - config["VapidKeys:Subject"], - config["VapidKeys:PublicKey"], - config["VapidKeys:PrivateKey"] - ); -}); +builder.Services.AddSingleton(); var app = builder.Build(); -// Configure the HTTP request pipeline. +app.UseStaticFiles(); +// Middleware: om ej konfigurerad → redirect till /setup +app.Use(async (context, next) => +{ + var setupService = context.RequestServices.GetRequiredService(); + var currentSetup = setupService.GetSetup(); + + var path = context.Request.Path; + var method = context.Request.Method; + + if (!currentSetup.IsConfigured && + !path.StartsWithSegments("/setup") && + !(path == "/setup" && method == "POST") && // 👈 tillåt POST till /setup + !path.StartsWithSegments("/api/setup")) + { + context.Response.Redirect("/setup"); + return; + } + + await next(); +}); + + + +// Configure the HTTP request pipeline if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } -app.UseStaticFiles(); + app.UseRouting(); app.UseSession(); -app.UseAuthentication();; +app.UseAuthentication(); app.UseAuthorization(); -// Map controller endpoints (including AJAX JSON actions) +// Routing app.MapControllers(); - -// Conventional MVC route app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); - -// Map Razor Pages app.MapRazorPages(); -using (var scope = app.Services.CreateScope()) + +// Init: migrera databas och skapa admin +if (setup.IsConfigured) { + using var scope = app.Services.CreateScope(); var services = scope.ServiceProvider; var context = services.GetRequiredService(); - // Vänta tills databasen är redo + int retries = 10; while (retries > 0) { try { - context.Database.OpenConnection(); // bara testa öppna anslutningen + context.Database.OpenConnection(); context.Database.CloseConnection(); - break; // lyckades! + break; } catch { @@ -164,17 +206,17 @@ using (var scope = app.Services.CreateScope()) } } - // Kör alla EF-migrationer automatiskt context.Database.Migrate(); - // Skapa standardroller och admin - await IdentityDataInitializer.SeedData(services); + var userManager = services.GetRequiredService>(); + var anyUsers = await userManager.Users.AnyAsync(); - // (valfritt) Lägg in exempelbudgetdata - // await TestDataSeeder.SeedBudget(context); + if (!anyUsers) + { + Console.WriteLine("🧩 Ingen användare hittades – skapar admin..."); + await IdentityDataInitializer.SeedData(services, setup); + } } - - app.Run(); diff --git a/Aberwyn/Properties/launchSettings.json b/Aberwyn/Properties/launchSettings.json index 66d0fc0..a663ecc 100644 --- a/Aberwyn/Properties/launchSettings.json +++ b/Aberwyn/Properties/launchSettings.json @@ -8,7 +8,7 @@ } }, "profiles": { - "Aberwyn (Dev)": { + "Aberwyn": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { @@ -16,14 +16,6 @@ }, "applicationUrl": "https://localhost:7290;http://localhost:5290" }, - "Aberwyn (Prod)": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production" - }, - "applicationUrl": "https://localhost:7290;http://localhost:5290" - }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, diff --git a/Aberwyn/Views/Setup/Index.cshtml b/Aberwyn/Views/Setup/Index.cshtml new file mode 100644 index 0000000..6a091aa --- /dev/null +++ b/Aberwyn/Views/Setup/Index.cshtml @@ -0,0 +1,395 @@ +@model Aberwyn.Models.SetupSettings +@{ + ViewData["Title"] = "Installera Aberwyn"; +} + +

Installera Aberwyn

+ +@if (ViewBag.Error != null) +{ +
@ViewBag.Error
+} + + + +
+
+

Steg 1: Databasinställningar

+
+ + +
Fältet är obligatoriskt
+
+
+ + +
Fältet är obligatoriskt
+
+
+ + +
Fältet är obligatoriskt
+
+
+ + +
Fältet är obligatoriskt
+
+
+ + +
Fältet är obligatoriskt
+
+ +
+ +
+ + + + + +
+ + +@if (User.IsInRole("Admin")) +{ +
+ +
+} + +@section Scripts { + + + + + +} diff --git a/Aberwyn/Views/Setup/SetupComplete.cshtml b/Aberwyn/Views/Setup/SetupComplete.cshtml new file mode 100644 index 0000000..220085f --- /dev/null +++ b/Aberwyn/Views/Setup/SetupComplete.cshtml @@ -0,0 +1,17 @@ +@{ + ViewData["Title"] = "Klart!"; +} + +

✅ Aberwyn är nu installerad

+

Du kan nu starta om applikationen för att börja använda den.

+ +Till startsidan + +
+ +

🔄 Återställ installation

+

Om du vill nollställa installationen och börja om från början, klicka här:

+ +
+ +
diff --git a/Aberwyn/Views/Shared/_Layout.cshtml b/Aberwyn/Views/Shared/_Layout.cshtml index 94388ae..e01e070 100644 --- a/Aberwyn/Views/Shared/_Layout.cshtml +++ b/Aberwyn/Views/Shared/_Layout.cshtml @@ -21,35 +21,46 @@ Ludwig - + @if (ViewBag.IsSetupMode as bool? != true) + { + + }
diff --git a/Aberwyn/Views/Shared/_LoginPartial.cshtml b/Aberwyn/Views/Shared/_LoginPartial.cshtml index 1f1c8f2..6987907 100644 --- a/Aberwyn/Views/Shared/_LoginPartial.cshtml +++ b/Aberwyn/Views/Shared/_LoginPartial.cshtml @@ -31,7 +31,7 @@ else
- +