diff --git a/.gitignore b/.gitignore index 0e5c9aa..bbc75d1 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,8 @@ __screenshots__/ .env.local .env.*.local *.local +API/appsettings.custom.json +API/appsettings.custom.*.json # Temporary files ~$* diff --git a/API/API.csproj b/API/API.csproj index 58e40dc..6d308c2 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -7,6 +7,11 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/API/Database/ApplicationDbContext.cs b/API/Database/ApplicationDbContext.cs new file mode 100644 index 0000000..5465878 --- /dev/null +++ b/API/Database/ApplicationDbContext.cs @@ -0,0 +1,5 @@ +using Microsoft.EntityFrameworkCore; + +namespace API.Database; + +public class ApplicationDbContext(DbContextOptions options) : DbContext(options); diff --git a/API/Dev/docker-compose.yml b/API/Dev/docker-compose.yml new file mode 100644 index 0000000..b41e3a8 --- /dev/null +++ b/API/Dev/docker-compose.yml @@ -0,0 +1,38 @@ +services: + postgres: + image: postgres:16-alpine + container_name: hoard-postgres + restart: unless-stopped + environment: + POSTGRES_DB: hoard + POSTGRES_USER: hoard + POSTGRES_PASSWORD: hoard_dev_password + ports: + - "5432:5432" + volumes: + - hoard_postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U hoard -d hoard"] + interval: 10s + timeout: 5s + retries: 5 + + pgadmin: + image: dpage/pgadmin4:9.3 + container_name: hoard-pgadmin + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: familiehimmelberg@gmail.com + PGADMIN_DEFAULT_PASSWORD: admin + PGADMIN_CONFIG_SERVER_MODE: "False" + ports: + - "5050:80" + depends_on: + postgres: + condition: service_healthy + volumes: + - hoard_pgadmin_data:/var/lib/pgadmin + +volumes: + hoard_postgres_data: + hoard_pgadmin_data: diff --git a/API/Migrations/20260418134949_InitialPostgres.Designer.cs b/API/Migrations/20260418134949_InitialPostgres.Designer.cs new file mode 100644 index 0000000..44f7c88 --- /dev/null +++ b/API/Migrations/20260418134949_InitialPostgres.Designer.cs @@ -0,0 +1,58 @@ +// +using System; +using API.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260418134949_InitialPostgres")] + partial class InitialPostgres + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("API.Models.Test.TestItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("test_items", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/20260418134949_InitialPostgres.cs b/API/Migrations/20260418134949_InitialPostgres.cs new file mode 100644 index 0000000..36c25e1 --- /dev/null +++ b/API/Migrations/20260418134949_InitialPostgres.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + /// + public partial class InitialPostgres : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "test_items", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_test_items", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "test_items"); + } + } +} diff --git a/API/Migrations/20260418153650_RemoveTestItems.Designer.cs b/API/Migrations/20260418153650_RemoveTestItems.Designer.cs new file mode 100644 index 0000000..4d3ef64 --- /dev/null +++ b/API/Migrations/20260418153650_RemoveTestItems.Designer.cs @@ -0,0 +1,29 @@ +// +using API.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260418153650_RemoveTestItems")] + partial class RemoveTestItems + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/20260418153650_RemoveTestItems.cs b/API/Migrations/20260418153650_RemoveTestItems.cs new file mode 100644 index 0000000..48bb8f0 --- /dev/null +++ b/API/Migrations/20260418153650_RemoveTestItems.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + /// + public partial class RemoveTestItems : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "test_items"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "test_items", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false), + Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + UpdatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_test_items", x => x.Id); + }); + } + } +} diff --git a/API/Migrations/ApplicationDbContextModelSnapshot.cs b/API/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..b22d9fb --- /dev/null +++ b/API/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,26 @@ +// +using API.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Program.cs b/API/Program.cs index 0028f4f..55cb5c1 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,10 +1,28 @@ +using API.Database; +using Microsoft.EntityFrameworkCore; + var builder = WebApplication.CreateBuilder(args); +builder.Configuration.AddJsonFile("appsettings.custom.json", optional: true, reloadOnChange: true); + +var connectionString = builder.Configuration.GetConnectionString("Postgres") + ?? throw new InvalidOperationException("Connection string 'Postgres' wurde nicht gefunden."); + +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(connectionString); +}); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.Migrate(); +} var webRootPath = app.Environment.WebRootPath ?? Path.Combine(app.Environment.ContentRootPath, "wwwroot"); var indexFilePath = Path.Combine(webRootPath, "index.html"); diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json index 0c208ae..2ea6405 100644 --- a/API/appsettings.Development.json +++ b/API/appsettings.Development.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "Postgres": "Host=localhost;Port=5432;Database=hoard;Username=hoard;Password=hoard_dev_password" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/API/appsettings.json b/API/appsettings.json index 10f68b8..411e4a4 100644 --- a/API/appsettings.json +++ b/API/appsettings.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "Postgres": "" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/README.md b/README.md index c0ef714..1353d49 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Hoard

- +

Hoard ist eine einfache, selbst gehostete Web-App zur Verwaltung von Dateien und Ordnern – mit integrierter Markdown-Bearbeitung direkt im Browser. diff --git a/codexInfo.md b/codexInfo.md index 162f83f..ce45853 100644 --- a/codexInfo.md +++ b/codexInfo.md @@ -90,6 +90,11 @@ Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für - Backend-API ist auf ein Minimal-Setup reduziert und stellt aktuell den Test-Endpunkt `GET /api/health` bereit. - Swagger/OpenAPI ist im Backend nur im Development-Modus aktiv (`/swagger`). - Frontend-Build (`npm run build` im `GUI`-Projekt) schreibt direkt nach `API/wwwroot`; das Backend liefert die SPA und statische Assets aus. +- Backend nutzt jetzt PostgreSQL über `ConnectionStrings:Postgres` mit EF Core (`ApplicationDbContext`) und führt Migrationen beim Start automatisch aus. +- Temporäre Test-Entity und Test-CRUD-Endpunkt (`api/test-items`) wurden wieder entfernt; aktuell bleibt der minimale Health-Endpunkt `GET /api/health`. +- Für lokale Entwicklung liegt unter `API/Dev/docker-compose.yml` ein Stack mit PostgreSQL (`localhost:5432`) und pgAdmin (`localhost:5050`). +- API lädt optional `API/appsettings.custom.json`; wenn vorhanden, überschreibt sie Werte aus `appsettings.json`. +- `API/appsettings.custom.json` ist in `.gitignore` hinterlegt, damit lokale Konfigurationswerte nicht versehentlich committed werden. ## Änderungen durch Codex - Grundlegender UI-Neuaufbau der App-Shell (`GUI/src/Layout.vue`) inklusive Navigation, Footer und Seitenkontext. @@ -111,3 +116,12 @@ Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für - `API/Program.cs` erweitert, damit statische Dateien aus `wwwroot` inkl. SPA-Fallback (`index.html`) ausgeliefert werden. - SPA-Fallback im Backend aufgeteilt: Frontend-Routen liefern `index.html`, unbekannte `/api/*`-Routen bleiben korrekt `404` statt auf die SPA zu fallen. - Swagger im Backend ergänzt: `Swashbuckle.AspNetCore` eingebunden, Services registriert und UI nur in `Development` aktiviert. +- PostgreSQL-Integration im Backend umgesetzt: `Npgsql.EntityFrameworkCore.PostgreSQL` + `Microsoft.EntityFrameworkCore.Design` in `API/API.csproj` ergänzt und Connection String `ConnectionStrings:Postgres` in den Settings hinterlegt. +- `API/Database/ApplicationDbContext.cs` mit Test-Entity `API/Models/Test/TestItem.cs` angelegt; erste Migrationen in `API/Migrations` erstellt. +- `API/Program.cs` um `AddDbContext` (Postgres) und `Database.Migrate()` beim Start erweitert, damit Migrationen automatisch angewendet werden. +- `API/Controllers/TestItemsController.cs` als einfacher CRUD-Testcontroller (`GET/POST/PUT/DELETE`) unter `api/test-items` ergänzt. +- Dev-Stack für lokale Datenbankarbeit ergänzt: `API/Dev/docker-compose.yml` startet PostgreSQL + pgAdmin. +- `API/Program.cs` so erweitert, dass optional `appsettings.custom.json` geladen wird und bei vorhandener Datei bevorzugte lokale Overrides möglich sind. +- Neue lokale Konfigurationsdatei `API/appsettings.custom.json` aus der bisherigen `appsettings.json` angelegt und in `.gitignore` ergänzt. +- Test-Datentyp und Test-API wieder entfernt: `API/Models/Test/TestItem.cs` und `API/Controllers/TestItemsController.cs` gelöscht, `ApplicationDbContext` bereinigt. +- Neue Migration `RemoveTestItems` erstellt (`API/Migrations/20260418153650_RemoveTestItems.cs`), die Tabelle `test_items` entfernt und den Snapshot aktualisiert. diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 0000000..3a299d9 --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.6", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file