Setup!
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-06-03 21:36:16 +02:00
parent 3d6aa2d424
commit 908bc469c6
14 changed files with 877 additions and 109 deletions

View File

@@ -9,6 +9,13 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Views\NewFolder\**" />
<Content Remove="Views\NewFolder\**" />
<EmbeddedResource Remove="Views\NewFolder\**" />
<None Remove="Views\NewFolder\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngularJS.Core" Version="1.8.2" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.67" />
@@ -20,6 +27,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.36" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.36" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.36">
<PrivateAssets>all</PrivateAssets>
@@ -42,7 +50,6 @@
<ItemGroup>
<Folder Include="Migrations\" />
<Folder Include="Views\NewFolder\" />
<Folder Include="wwwroot\images\meals\" />
</ItemGroup>

View File

@@ -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; }
}
}
}

View File

@@ -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<IActionResult> 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<ApplicationDbContext>();
await db.Database.MigrateAsync();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
// 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();
}
}

View File

@@ -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<IdentityResult> SeedData(IServiceProvider serviceProvider, SetupSettings? setup = null)
{
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var config = serviceProvider.GetService<IConfiguration>();
if (setup == null && config != null)
{
setup = config.GetSection("SetupSettings").Get<SetupSettings>() ?? 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;
}
}
}
}

View File

@@ -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<SetupSettings>(json) ?? new SetupSettings { IsConfigured = false };
}
internal static IServiceProvider BuildTemporaryServices(string connectionString)
{
var services = new ServiceCollection();
// Konfigurera EF + Identity
services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Lägg till en tom konfiguration för att undvika null
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
// Valfritt: Lägg till loggning om något kräver det
services.AddLogging();
return services.BuildServiceProvider();
}
}
}

View File

@@ -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"
}

View File

@@ -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; }
}
}

View File

@@ -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<SetupSettings>(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<65>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<ApplicationDbContext>(options =>
options.UseMySql(
builder.Configuration.GetConnectionString("DefaultConnection"),
new MySqlServerVersion(new Version(8, 0, 36)),
mySqlOptions => mySqlOptions.EnableRetryOnFailure()
)
);
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
// Registrera rätt databas och identity beroende på om setup är klar
if (setup.IsConfigured)
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
string connectionString = $"Server={setup.DbHost};Port={setup.DbPort};Database={setup.DbName};User={setup.DbUser};Password={setup.DbPassword};";
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
}
else
{
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("TempSetup"));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
}
// Identity inställningar
builder.Services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
@@ -74,8 +104,7 @@ builder.Services.Configure<IdentityOptions>(options =>
options.Password.RequireUppercase = false;
});
builder.Services.AddControllersWithViews();
// Register your services
// Appens övriga tjänster
builder.Services.AddScoped<MenuService>();
builder.Services.AddSingleton<PushNotificationService>(sp =>
@@ -89,11 +118,14 @@ builder.Services.AddSingleton<PushNotificationService>(sp =>
});
builder.Services.Configure<VapidOptions>(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<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[] { new CultureInfo("sv-SE") };
@@ -101,60 +133,70 @@ builder.Services.Configure<RequestLocalizationOptions>(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<PushNotificationService>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return new PushNotificationService(
config["VapidKeys:Subject"],
config["VapidKeys:PublicKey"],
config["VapidKeys:PrivateKey"]
);
});
builder.Services.AddSingleton<SetupService>();
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<SetupService>();
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<ApplicationDbContext>();
// 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<UserManager<ApplicationUser>>();
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();

View File

@@ -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,

View File

@@ -0,0 +1,395 @@
@model Aberwyn.Models.SetupSettings
@{
ViewData["Title"] = "Installera Aberwyn";
}
<h1>Installera Aberwyn</h1>
@if (ViewBag.Error != null)
{
<div class="alert alert-danger">@ViewBag.Error</div>
}
<style>
.setup-step {
margin-bottom: 2rem;
padding: 1.5rem;
background-color: #f8f9fa;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
.form-group {
margin-bottom: 1rem;
display: flex;
flex-direction: column;
}
label {
font-weight: bold;
margin-bottom: 0.2rem;
}
input.form-control {
padding: 0.5rem;
font-size: 1rem;
border-radius: 6px;
}
.is-invalid {
border-color: #dc3545;
}
.invalid-feedback {
color: #dc3545;
font-size: 0.875rem;
display: none;
}
.is-invalid + .invalid-feedback {
display: block;
}
button {
padding: 10px 20px;
font-size: 1rem;
border-radius: 6px;
border: none;
cursor: pointer;
transition: background 0.2s ease;
margin-top: 0.5rem;
margin-right: 0.5rem;
}
.btn-primary { background-color: #6a0dad; color: white; }
.btn-primary:hover { background-color: #5a0cab; }
.btn-secondary { background-color: #adb5bd; color: black; }
.btn-secondary:hover { background-color: #9ca3af; }
.btn-success { background-color: #198754; color: white; }
.btn-success:hover { background-color: #157347; }
.btn-danger { background-color: #dc3545; color: white; }
.btn-danger:hover { background-color: #bb2d3b; }
.btn-outline-info {
border: 1px solid #0dcaf0;
color: #0dcaf0;
background: none;
}
.btn-outline-info:hover {
background-color: #0dcaf0;
color: white;
}
#setup-summary ul {
list-style: none;
padding-left: 0;
}
#setup-summary li {
margin-bottom: 0.4rem;
}
.spinner {
display: inline-block;
width: 1.2rem;
height: 1.2rem;
border: 3px solid rgba(0,0,0,0.2);
border-top-color: #6a0dad;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 0.5rem;
}
@@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<form method="post" id="setup-form">
<div id="step-1" class="setup-step">
<h2>Steg 1: Databasinställningar</h2>
<div class="form-group">
<label for="DbHost">Server</label>
<input asp-for="DbHost" name="DbHost" class="form-control" id="DbHost" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="DbPort">Port</label>
<input asp-for="DbPort" name="DbPort" class="form-control" id="DbPort" value="3306" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="DbName">Databasnamn</label>
<input asp-for="DbName" name="DbName" class="form-control" id="DbName" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="DbUser">Användarnamn</label>
<input asp-for="DbUser" name="DbUser" class="form-control" id="DbUser" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="DbPassword">Lösenord</label>
<input asp-for="DbPassword" name="DbPassword" type="password" class="form-control" id="DbPassword" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<button type="button" class="btn btn-outline-info" onclick="testDbConnection()">Testa anslutning</button>
<div id="db-test-result" class="mt-2"></div>
<button type="button" class="btn btn-primary" onclick="validateStep(1) && goToStep(2)">Nästa</button>
</div>
<div id="step-2" class="setup-step" style="display: none;">
<h2>Steg 2: Administratör</h2>
<div class="form-group">
<label for="AdminUsername">Admin Användarnamn</label>
<input asp-for="AdminUsername" name="AdminUsername" class="form-control" id="AdminUsername" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="AdminEmail">E-postadress</label>
<input asp-for="AdminEmail" name="AdminEmail" type="email" class="form-control" id="AdminEmail" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="AdminPassword">Lösenord</label>
<input asp-for="AdminPassword" name="AdminPassword" type="password" class="form-control"
id="AdminPassword"
pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d]).{6,}"
title="Minst 6 tecken, inklusive versal, gemen, siffra och specialtecken"
required />
<div class="invalid-feedback">Ange ett giltigt lösenord enligt kraven</div>
</div>
<div class="form-group">
<label for="ConfirmPassword">Bekräfta lösenord</label>
<input type="password" id="ConfirmPassword" class="form-control" required />
<span id="password-match-status" style="margin-top: 4px; font-size: 1.2rem;"></span>
<div class="invalid-feedback" id="confirmPasswordError">Lösenorden matchar inte</div>
</div>
<ul id="password-criteria" style="list-style: none; padding-left: 0; font-size: 0.9rem; margin-top: 0.5rem;">
<li id="length-criteria">❌ Minst 6 tecken</li>
<li id="uppercase-criteria">❌ Minst en stor bokstav (AZ)</li>
<li id="lowercase-criteria">❌ Minst en liten bokstav (az)</li>
<li id="digit-criteria">❌ Minst en siffra (09)</li>
<li id="special-criteria">❌ Minst ett specialtecken (!#&...)</li>
</ul>
<button type="button" class="btn btn-secondary" onclick="goToStep(1)">Tillbaka</button>
<button type="button" class="btn btn-primary" onclick="validateAdminStep() && goToStep(3)">Nästa</button>
</div>
<div id="step-3" class="setup-step" style="display: none;">
<h2>Steg 3: Konfiguration pågår...</h2>
<div id="setup-summary">
<ul>
<li><strong>Server:</strong> <span id="summary-DbHost"></span></li>
<li><strong>Port:</strong> <span id="summary-DbPort"></span></li>
<li><strong>Databas:</strong> <span id="summary-DbName"></span></li>
<li><strong>Användare:</strong> <span id="summary-DbUser"></span></li>
<li><strong>Adminnamn:</strong> <span id="summary-AdminUsername"></span></li>
<li><strong>Admin-e-post:</strong> <span id="summary-AdminEmail"></span></li>
</ul>
</div>
<div id="setup-progress" class="mt-3">
<p>
Skapar databastabeller och konfigurerar administratörskonto...
<span class="spinner"></span>
</p>
</div>
<button type="button" class="btn btn-secondary" onclick="goToStep(2)">Tillbaka</button>
<button type="button" class="btn btn-success" onclick="submitSetup()">Slutför installation</button>
</div>
</form>
@if (User.IsInRole("Admin"))
{
<form method="post" asp-action="Reset" class="mt-3" onsubmit="return confirm('Är du säker på att du vill återställa konfigurationen?')">
<button type="submit" class="btn btn-danger">Återställ inställningar</button>
</form>
}
@section Scripts {
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.5/jquery.validate.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate-unobtrusive/3.2.12/jquery.validate.unobtrusive.min.js"></script>
<script>
function goToStep(step) {
document.querySelectorAll('.setup-step').forEach(s => s.style.display = 'none');
document.getElementById('step-' + step).style.display = 'block';
if (step === 3) {
document.getElementById('summary-DbHost').textContent = document.getElementById('DbHost').value;
document.getElementById('summary-DbPort').textContent = document.getElementById('DbPort').value;
document.getElementById('summary-DbName').textContent = document.getElementById('DbName').value;
document.getElementById('summary-DbUser').textContent = document.getElementById('DbUser').value;
document.getElementById('summary-AdminUsername').textContent = document.getElementById('AdminUsername').value;
document.getElementById('summary-AdminEmail').textContent = document.getElementById('AdminEmail').value;
document.getElementById('setup-progress').innerHTML = `<p>Redo att påbörja installationen.</p>`;
}
}
document.getElementById('AdminPassword').addEventListener('input', function () {
const val = this.value;
const hasLength = val.length >= 6;
const hasUpper = /[A-Z]/.test(val);
const hasLower = /[a-z]/.test(val);
const hasDigit = /\d/.test(val);
const hasSpecial = /[!@@#$%^&*(),.?":{}|<>_\-\\[\]\/+=~`]/.test(val); // lägg till specialtecken
updateCriteria('length-criteria', hasLength);
updateCriteria('uppercase-criteria', hasUpper);
updateCriteria('lowercase-criteria', hasLower);
updateCriteria('digit-criteria', hasDigit);
updateCriteria('special-criteria', hasSpecial);
});
function updateCriteria(id, isValid) {
const el = document.getElementById(id);
if (isValid) {
el.textContent = '✔ ' + el.textContent.slice(2);
el.style.color = 'green';
} else {
el.textContent = '❌ ' + el.textContent.slice(2);
el.style.color = 'red';
}
}
function validateStep(step) {
let isValid = true;
document.querySelectorAll('#step-' + step + ' [required]').forEach(field => {
if (!field.value.trim()) {
field.classList.add('is-invalid');
isValid = false;
} else {
field.classList.remove('is-invalid');
}
});
return isValid;
}
function testDbConnection() {
const host = document.getElementById('DbHost').value;
const port = document.getElementById('DbPort').value;
const db = document.getElementById('DbName').value;
const user = document.getElementById('DbUser').value;
const pass = document.getElementById('DbPassword').value;
const output = document.getElementById('db-test-result');
output.innerHTML = '<span class="spinner"></span> Testar...';
fetch('/api/setup/testdb', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ host, port, db, user, pass })
})
.then(async res => {
const text = await res.text();
console.log("Raw:", text);
try {
const result = JSON.parse(text);
if (result.success) {
output.innerHTML = '✅ <span class="text-success">Anslutning lyckades!</span>';
} else {
output.innerHTML = '❌ <span class="text-danger">' + result.message + '</span>';
}
} catch (e) {
output.innerHTML = '❌ <span class="text-danger">Felaktigt svar från servern: ' + text + '</span>';
}
})
.catch(err => {
output.innerHTML = '❌ <span class="text-danger">Nätverksfel: ' + err.message + '</span>';
});
}
function validateAdminStep() {
let valid = validateStep(2);
const pass = document.getElementById('AdminPassword');
const confirm = document.getElementById('ConfirmPassword');
const confirmError = document.getElementById('confirmPasswordError');
if (pass.value !== confirm.value) {
confirm.classList.add('is-invalid');
confirmError.style.display = 'block';
valid = false;
} else {
confirm.classList.remove('is-invalid');
confirmError.style.display = 'none';
}
return valid;
}
document.getElementById('ConfirmPassword').addEventListener('input', function () {
const pass = document.getElementById('AdminPassword').value;
const confirm = this.value;
const confirmField = document.getElementById('ConfirmPassword');
const matchStatus = document.getElementById('password-match-status');
if (confirm.length === 0) {
matchStatus.innerHTML = '';
confirmField.classList.remove('is-invalid');
return;
}
if (pass === confirm) {
matchStatus.innerHTML = '✔';
matchStatus.style.color = 'green';
confirmField.classList.remove('is-invalid');
} else {
matchStatus.innerHTML = '❌';
matchStatus.style.color = 'red';
confirmField.classList.add('is-invalid');
}
});
async function submitSetup() {
goToStep(3);
const form = document.getElementById('setup-form');
const formData = new FormData(form);
const data = {};
formData.forEach((value, key) => { data[key] = value; });
const resultEl = document.getElementById('setup-progress');
resultEl.innerHTML = `<p>
Skapar databastabeller och konfigurerar administratörskonto...
<span class="spinner"></span>
</p>`;
try {
const response = await fetch('/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const text = await response.text();
let result;
try {
result = JSON.parse(text);
} catch (err) {
throw new Error("Kunde inte tolka JSON-svar: " + text);
}
if (!response.ok) {
resultEl.innerHTML = `
<div class="alert alert-danger mt-3">
❌ Fel vid installation: ${result.error || 'Okänt fel'}
</div>`;
return;
}
resultEl.innerHTML = `
<div class="alert alert-success mt-3">
✅ ${result.message || 'Installation slutförd!'}
<br />
<button class="btn btn-success mt-3" onclick="window.location.href='/'">Gå vidare</button>
</div>`;
} catch (err) {
console.error("Något gick fel:", err);
resultEl.innerHTML = `<div class="alert alert-danger mt-3">
❌ Fel vid installation: ${err.message}
</div>`;
}
}
</script>
}

View File

@@ -0,0 +1,17 @@
@{
ViewData["Title"] = "Klart!";
}
<h1>✅ Aberwyn är nu installerad</h1>
<p>Du kan nu starta om applikationen för att börja använda den.</p>
<a href="/" class="btn btn-primary">Till startsidan</a>
<hr />
<h3>🔄 Återställ installation</h3>
<p>Om du vill nollställa installationen och börja om från början, klicka här:</p>
<form method="post" asp-action="Reset">
<button type="submit" class="btn btn-danger">Återställ konfiguration</button>
</form>

View File

@@ -21,35 +21,46 @@
<span class="initial L2">L</span><span class="name">udwig</span>
</h1>
</div>
<partial name="_LoginPartial" />
@if (ViewBag.IsSetupMode as bool? != true)
{
<partial name="_LoginPartial" />
}
</header>
<div class="page-content">
<aside class="sidebar">
<ul class="nav-list">
<li><a asp-controller="Home" asp-action="Index"><i class="fas fa-home"></i> Home</a></li>
@if (User.IsInRole("Admin") || User.IsInRole("Budget"))
{
<li><a asp-controller="Budget" asp-action="Index"><i class="fas fa-wallet"></i> Budget</a></li>
}
<li><a asp-controller="Home" asp-action="Menu"><i class="fas fa-utensils"></i> Veckomeny</a></li>
@if (ViewBag.RestaurantIsOpen as bool? == true)
{
<li><a asp-controller="FoodMenu" asp-action="PizzaOrder"><i class="fas fa-pizza-slice"></i> Beställ pizza</a></li>
}
@if (User.IsInRole("Admin") || User.IsInRole("Chef"))
{
<li><a asp-controller="FoodMenu" asp-action="Veckomeny"><i class="fas fa-calendar-week"></i> Administrera Veckomeny</a></li>
<li><a asp-controller="FoodMenu" asp-action="MealAdmin"><i class="fas fa-list"></i> Måltider</a></li>
<li><a asp-controller="FoodMenu" asp-action="PizzaAdmin"><i class="fas fa-list"></i> Pizza Admin</a></li>
@if (ViewBag.IsSetupMode as bool? != true)
{
<ul class="nav-list">
<li><a asp-controller="Home" asp-action="Index"><i class="fas fa-home"></i> Home</a></li>
@if (User.IsInRole("Admin") || User.IsInRole("Budget"))
{
<li><a asp-controller="Budget" asp-action="Index"><i class="fas fa-wallet"></i> Budget</a></li>
}
<li><a asp-controller="Home" asp-action="Menu"><i class="fas fa-utensils"></i> Veckomeny</a></li>
@if (ViewBag.RestaurantIsOpen as bool? == true)
{
<li><a asp-controller="FoodMenu" asp-action="PizzaOrder"><i class="fas fa-pizza-slice"></i> Beställ pizza</a></li>
}
@if (User.IsInRole("Admin") || User.IsInRole("Chef"))
{
<li><a asp-controller="FoodMenu" asp-action="Veckomeny"><i class="fas fa-calendar-week"></i> Administrera Veckomeny</a></li>
<li><a asp-controller="FoodMenu" asp-action="MealAdmin"><i class="fas fa-list"></i> Måltider</a></li>
<li><a asp-controller="FoodMenu" asp-action="PizzaAdmin"><i class="fas fa-list"></i> Pizza Admin</a></li>
}
}
@if (User.IsInRole("Admin"))
{
<li><a asp-controller="Admin" asp-action="Index"><i class="fas fa-cog"></i> Adminpanel</a></li>
}
</ul>
@if (User.IsInRole("Admin"))
{
<li><a asp-controller="Admin" asp-action="Index"><i class="fas fa-cog"></i> Adminpanel</a></li>
}
</ul>
} else
{
<ul class="nav-list">
<li><a asp-controller="Setup" asp-action="Index"> Setup</a></li>
</ul>
}
</aside>
<main class="main-panel">

View File

@@ -31,7 +31,7 @@ else
<div class="auth-dropdown">
<form method="post" asp-area="Identity" asp-page="/Account/Login" style="padding: 12px; display: flex; flex-direction: column; gap: 8px;">
<input type="hidden" name="ReturnUrl" value="/" />
<input name="Input.Email" type="email" placeholder="E-post" required style="padding: 6px; font-size: 14px;" />
<input name="Input.Username" type="text" placeholder="Användarnamn" required style="padding: 6px; font-size: 14px;" />
<input name="Input.Password" type="password" placeholder="Lösenord" required style="padding: 6px; font-size: 14px;" />
<button type="submit" style="background-color: #3A4E62; color: white; border: none; padding: 6px 10px; border-radius: 4px; font-size: 14px;">
Logga in

View File

@@ -13,5 +13,6 @@
"Subject": "mailto:e@zcz.se",
"PublicKey": "BBLmMdU3X3e79SqzAy4vIAJI0jmzRME17F9UKbO8XT1dfnO-mWIPKIrFDbIZD4_3ic7uoijK61vaGdfFUk3HUfU",
"PrivateKey": "oranoCmCo8HXdc03juNgbeSlKE39N3DYus_eMunLsnc"
}
},
}