diff --git a/BlueLaminate/BlueLaminate.EFCore/BlueLaminate.EFCore.csproj b/BlueLaminate/BlueLaminate.EFCore/BlueLaminate.EFCore.csproj index 264f4e1..2c78733 100644 --- a/BlueLaminate/BlueLaminate.EFCore/BlueLaminate.EFCore.csproj +++ b/BlueLaminate/BlueLaminate.EFCore/BlueLaminate.EFCore.csproj @@ -5,14 +5,22 @@ net10.0 enable enable + a768c8b7-60e5-4914-8594-db709ad9929c + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all + + + diff --git a/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContext.cs b/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContext.cs index f0bd2da..abd788e 100644 --- a/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContext.cs +++ b/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContext.cs @@ -28,8 +28,13 @@ public class SkinTrackerDbContext : DbContext public DbSet TradeItems => Set(); public DbSet PriceHistories => Set(); + /// The PostgreSQL schema that owns all of this context's tables. + public const string Schema = "skintracker"; + protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.HasDefaultSchema(Schema); + modelBuilder.ApplyConfiguration(new SkinConfiguration()); modelBuilder.ApplyConfiguration(new SkinConditionConfiguration()); modelBuilder.ApplyConfiguration(new SteamUserConfiguration()); diff --git a/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContextFactory.cs b/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContextFactory.cs index 5a3f7a0..427b7b1 100644 --- a/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContextFactory.cs +++ b/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContextFactory.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; namespace BlueLaminate.EFCore.Data; @@ -7,15 +8,25 @@ public class SkinTrackerDbContextFactory : IDesignTimeDbContextFactory() - .UseNpgsql(connectionString) + .UseNpgsql(connectionString, npgsql => + npgsql.MigrationsHistoryTable("__EFMigrationsHistory", SkinTrackerDbContext.Schema)) .UseSnakeCaseNamingConvention() .Options; return new SkinTrackerDbContext(options); } + + public static IConfiguration BuildConfiguration() => + new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: true) + .AddUserSecrets(optional: true) + .AddEnvironmentVariables() + .Build(); } diff --git a/BlueLaminate/BlueLaminate.EFCore/DependencyInjection/ServiceCollectionExtensions.cs b/BlueLaminate/BlueLaminate.EFCore/DependencyInjection/ServiceCollectionExtensions.cs index 930331f..90abb3f 100644 --- a/BlueLaminate/BlueLaminate.EFCore/DependencyInjection/ServiceCollectionExtensions.cs +++ b/BlueLaminate/BlueLaminate.EFCore/DependencyInjection/ServiceCollectionExtensions.cs @@ -12,7 +12,8 @@ public static class ServiceCollectionExtensions { services.AddDbContext(options => options - .UseNpgsql(connectionString) + .UseNpgsql(connectionString, npgsql => + npgsql.MigrationsHistoryTable("__EFMigrationsHistory", SkinTrackerDbContext.Schema)) .UseSnakeCaseNamingConvention()); return services; diff --git a/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.Designer.cs b/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529180200_InitialCreate.Designer.cs similarity index 97% rename from BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.Designer.cs rename to BlueLaminate/BlueLaminate.EFCore/Migrations/20260529180200_InitialCreate.Designer.cs index c7edb91..a027769 100644 --- a/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.Designer.cs +++ b/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529180200_InitialCreate.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace BlueLaminate.EFCore.Migrations { [DbContext(typeof(SkinTrackerDbContext))] - [Migration("20260529170710_InitialCreate")] + [Migration("20260529180200_InitialCreate")] partial class InitialCreate { /// @@ -20,6 +20,7 @@ namespace BlueLaminate.EFCore.Migrations { #pragma warning disable 612, 618 modelBuilder + .HasDefaultSchema("skintracker") .HasAnnotation("ProductVersion", "10.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); @@ -63,7 +64,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("UserId") .HasDatabaseName("ix_inventory_items_user_id"); - b.ToTable("inventory_items", (string)null); + b.ToTable("inventory_items", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.PriceHistory", b => @@ -111,7 +112,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("SkinId", "ConditionId", "RecordedAt") .HasDatabaseName("ix_price_histories_skin_id_condition_id_recorded_at"); - b.ToTable("price_histories", (string)null); + b.ToTable("price_histories", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.Skin", b => @@ -172,7 +173,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("WeaponId") .HasDatabaseName("ix_skins_weapon_id"); - b.ToTable("skins", (string)null); + b.ToTable("skins", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinCondition", b => @@ -207,7 +208,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("SkinId") .HasDatabaseName("ix_skin_conditions_skin_id"); - b.ToTable("skin_conditions", (string)null); + b.ToTable("skin_conditions", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinInstance", b => @@ -263,7 +264,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("SkinId") .HasDatabaseName("ix_skin_instances_skin_id"); - b.ToTable("skin_instances", (string)null); + b.ToTable("skin_instances", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.SteamUser", b => @@ -295,7 +296,7 @@ namespace BlueLaminate.EFCore.Migrations .IsUnique() .HasDatabaseName("ix_steam_users_steam_id"); - b.ToTable("steam_users", (string)null); + b.ToTable("steam_users", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.Trade", b => @@ -332,7 +333,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("ToUserId") .HasDatabaseName("ix_trades_to_user_id"); - b.ToTable("trades", (string)null); + b.ToTable("trades", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.TradeItem", b => @@ -361,7 +362,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("TradeId") .HasDatabaseName("ix_trade_items_trade_id"); - b.ToTable("trade_items", (string)null); + b.ToTable("trade_items", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.Weapon", b => @@ -391,7 +392,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasKey("Id") .HasName("pk_weapons"); - b.ToTable("weapons", (string)null); + b.ToTable("weapons", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b => diff --git a/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.cs b/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529180200_InitialCreate.cs similarity index 86% rename from BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.cs rename to BlueLaminate/BlueLaminate.EFCore/Migrations/20260529180200_InitialCreate.cs index 9b237fc..44d9adf 100644 --- a/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.cs +++ b/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529180200_InitialCreate.cs @@ -12,8 +12,12 @@ namespace BlueLaminate.EFCore.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.EnsureSchema( + name: "skintracker"); + migrationBuilder.CreateTable( name: "steam_users", + schema: "skintracker", columns: table => new { id = table.Column(type: "integer", nullable: false) @@ -29,6 +33,7 @@ namespace BlueLaminate.EFCore.Migrations migrationBuilder.CreateTable( name: "weapons", + schema: "skintracker", columns: table => new { id = table.Column(type: "integer", nullable: false) @@ -44,6 +49,7 @@ namespace BlueLaminate.EFCore.Migrations migrationBuilder.CreateTable( name: "trades", + schema: "skintracker", columns: table => new { id = table.Column(type: "integer", nullable: false) @@ -59,12 +65,14 @@ namespace BlueLaminate.EFCore.Migrations table.ForeignKey( name: "fk_trades_steam_users_from_user_id", column: x => x.from_user_id, + principalSchema: "skintracker", principalTable: "steam_users", principalColumn: "id", onDelete: ReferentialAction.Restrict); table.ForeignKey( name: "fk_trades_steam_users_to_user_id", column: x => x.to_user_id, + principalSchema: "skintracker", principalTable: "steam_users", principalColumn: "id", onDelete: ReferentialAction.Restrict); @@ -72,6 +80,7 @@ namespace BlueLaminate.EFCore.Migrations migrationBuilder.CreateTable( name: "skins", + schema: "skintracker", columns: table => new { id = table.Column(type: "integer", nullable: false) @@ -91,6 +100,7 @@ namespace BlueLaminate.EFCore.Migrations table.ForeignKey( name: "fk_skins_weapons_weapon_id", column: x => x.weapon_id, + principalSchema: "skintracker", principalTable: "weapons", principalColumn: "id", onDelete: ReferentialAction.Cascade); @@ -98,6 +108,7 @@ namespace BlueLaminate.EFCore.Migrations migrationBuilder.CreateTable( name: "skin_conditions", + schema: "skintracker", columns: table => new { id = table.Column(type: "integer", nullable: false) @@ -113,6 +124,7 @@ namespace BlueLaminate.EFCore.Migrations table.ForeignKey( name: "fk_skin_conditions_skins_skin_id", column: x => x.skin_id, + principalSchema: "skintracker", principalTable: "skins", principalColumn: "id", onDelete: ReferentialAction.Cascade); @@ -120,6 +132,7 @@ namespace BlueLaminate.EFCore.Migrations migrationBuilder.CreateTable( name: "price_histories", + schema: "skintracker", columns: table => new { id = table.Column(type: "integer", nullable: false) @@ -137,12 +150,14 @@ namespace BlueLaminate.EFCore.Migrations table.ForeignKey( name: "fk_price_histories_skin_conditions_condition_id", column: x => x.condition_id, + principalSchema: "skintracker", principalTable: "skin_conditions", principalColumn: "id", onDelete: ReferentialAction.Restrict); table.ForeignKey( name: "fk_price_histories_skins_skin_id", column: x => x.skin_id, + principalSchema: "skintracker", principalTable: "skins", principalColumn: "id", onDelete: ReferentialAction.Cascade); @@ -150,6 +165,7 @@ namespace BlueLaminate.EFCore.Migrations migrationBuilder.CreateTable( name: "skin_instances", + schema: "skintracker", columns: table => new { id = table.Column(type: "integer", nullable: false) @@ -168,12 +184,14 @@ namespace BlueLaminate.EFCore.Migrations table.ForeignKey( name: "fk_skin_instances_skin_conditions_condition_id", column: x => x.condition_id, + principalSchema: "skintracker", principalTable: "skin_conditions", principalColumn: "id", onDelete: ReferentialAction.Restrict); table.ForeignKey( name: "fk_skin_instances_skins_skin_id", column: x => x.skin_id, + principalSchema: "skintracker", principalTable: "skins", principalColumn: "id", onDelete: ReferentialAction.Cascade); @@ -181,6 +199,7 @@ namespace BlueLaminate.EFCore.Migrations migrationBuilder.CreateTable( name: "inventory_items", + schema: "skintracker", columns: table => new { id = table.Column(type: "integer", nullable: false) @@ -196,12 +215,14 @@ namespace BlueLaminate.EFCore.Migrations table.ForeignKey( name: "fk_inventory_items_skin_instances_skin_instance_id", column: x => x.skin_instance_id, + principalSchema: "skintracker", principalTable: "skin_instances", principalColumn: "id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "fk_inventory_items_steam_users_user_id", column: x => x.user_id, + principalSchema: "skintracker", principalTable: "steam_users", principalColumn: "id", onDelete: ReferentialAction.Cascade); @@ -209,6 +230,7 @@ namespace BlueLaminate.EFCore.Migrations migrationBuilder.CreateTable( name: "trade_items", + schema: "skintracker", columns: table => new { id = table.Column(type: "integer", nullable: false) @@ -222,12 +244,14 @@ namespace BlueLaminate.EFCore.Migrations table.ForeignKey( name: "fk_trade_items_inventory_items_inventory_item_id", column: x => x.inventory_item_id, + principalSchema: "skintracker", principalTable: "inventory_items", principalColumn: "id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "fk_trade_items_trades_trade_id", column: x => x.trade_id, + principalSchema: "skintracker", principalTable: "trades", principalColumn: "id", onDelete: ReferentialAction.Cascade); @@ -235,87 +259,104 @@ namespace BlueLaminate.EFCore.Migrations migrationBuilder.CreateIndex( name: "ix_inventory_items_asset_id", + schema: "skintracker", table: "inventory_items", column: "asset_id"); migrationBuilder.CreateIndex( name: "ix_inventory_items_skin_instance_id", + schema: "skintracker", table: "inventory_items", column: "skin_instance_id"); migrationBuilder.CreateIndex( name: "ix_inventory_items_user_id", + schema: "skintracker", table: "inventory_items", column: "user_id"); migrationBuilder.CreateIndex( name: "ix_price_histories_condition_id", + schema: "skintracker", table: "price_histories", column: "condition_id"); migrationBuilder.CreateIndex( name: "ix_price_histories_skin_id_condition_id_recorded_at", + schema: "skintracker", table: "price_histories", columns: new[] { "skin_id", "condition_id", "recorded_at" }); migrationBuilder.CreateIndex( name: "ix_skin_conditions_skin_id", + schema: "skintracker", table: "skin_conditions", column: "skin_id"); migrationBuilder.CreateIndex( name: "ix_skin_instances_condition_id", + schema: "skintracker", table: "skin_instances", column: "condition_id"); migrationBuilder.CreateIndex( name: "ix_skin_instances_float_value", + schema: "skintracker", table: "skin_instances", column: "float_value"); migrationBuilder.CreateIndex( name: "ix_skin_instances_paint_seed", + schema: "skintracker", table: "skin_instances", column: "paint_seed"); migrationBuilder.CreateIndex( name: "ix_skin_instances_skin_id", + schema: "skintracker", table: "skin_instances", column: "skin_id"); migrationBuilder.CreateIndex( name: "ix_skins_true_float", + schema: "skintracker", table: "skins", column: "true_float"); migrationBuilder.CreateIndex( name: "ix_skins_weapon_id", + schema: "skintracker", table: "skins", column: "weapon_id"); migrationBuilder.CreateIndex( name: "ix_steam_users_steam_id", + schema: "skintracker", table: "steam_users", column: "steam_id", unique: true); migrationBuilder.CreateIndex( name: "ix_trade_items_inventory_item_id", + schema: "skintracker", table: "trade_items", column: "inventory_item_id"); migrationBuilder.CreateIndex( name: "ix_trade_items_trade_id", + schema: "skintracker", table: "trade_items", column: "trade_id"); migrationBuilder.CreateIndex( name: "ix_trades_from_user_id", + schema: "skintracker", table: "trades", column: "from_user_id"); migrationBuilder.CreateIndex( name: "ix_trades_to_user_id", + schema: "skintracker", table: "trades", column: "to_user_id"); } @@ -324,31 +365,40 @@ namespace BlueLaminate.EFCore.Migrations protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "price_histories"); + name: "price_histories", + schema: "skintracker"); migrationBuilder.DropTable( - name: "trade_items"); + name: "trade_items", + schema: "skintracker"); migrationBuilder.DropTable( - name: "inventory_items"); + name: "inventory_items", + schema: "skintracker"); migrationBuilder.DropTable( - name: "trades"); + name: "trades", + schema: "skintracker"); migrationBuilder.DropTable( - name: "skin_instances"); + name: "skin_instances", + schema: "skintracker"); migrationBuilder.DropTable( - name: "steam_users"); + name: "steam_users", + schema: "skintracker"); migrationBuilder.DropTable( - name: "skin_conditions"); + name: "skin_conditions", + schema: "skintracker"); migrationBuilder.DropTable( - name: "skins"); + name: "skins", + schema: "skintracker"); migrationBuilder.DropTable( - name: "weapons"); + name: "weapons", + schema: "skintracker"); } } } diff --git a/BlueLaminate/BlueLaminate.EFCore/Migrations/SkinTrackerDbContextModelSnapshot.cs b/BlueLaminate/BlueLaminate.EFCore/Migrations/SkinTrackerDbContextModelSnapshot.cs index 4c686a4..4c62ecc 100644 --- a/BlueLaminate/BlueLaminate.EFCore/Migrations/SkinTrackerDbContextModelSnapshot.cs +++ b/BlueLaminate/BlueLaminate.EFCore/Migrations/SkinTrackerDbContextModelSnapshot.cs @@ -17,6 +17,7 @@ namespace BlueLaminate.EFCore.Migrations { #pragma warning disable 612, 618 modelBuilder + .HasDefaultSchema("skintracker") .HasAnnotation("ProductVersion", "10.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); @@ -60,7 +61,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("UserId") .HasDatabaseName("ix_inventory_items_user_id"); - b.ToTable("inventory_items", (string)null); + b.ToTable("inventory_items", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.PriceHistory", b => @@ -108,7 +109,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("SkinId", "ConditionId", "RecordedAt") .HasDatabaseName("ix_price_histories_skin_id_condition_id_recorded_at"); - b.ToTable("price_histories", (string)null); + b.ToTable("price_histories", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.Skin", b => @@ -169,7 +170,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("WeaponId") .HasDatabaseName("ix_skins_weapon_id"); - b.ToTable("skins", (string)null); + b.ToTable("skins", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinCondition", b => @@ -204,7 +205,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("SkinId") .HasDatabaseName("ix_skin_conditions_skin_id"); - b.ToTable("skin_conditions", (string)null); + b.ToTable("skin_conditions", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinInstance", b => @@ -260,7 +261,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("SkinId") .HasDatabaseName("ix_skin_instances_skin_id"); - b.ToTable("skin_instances", (string)null); + b.ToTable("skin_instances", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.SteamUser", b => @@ -292,7 +293,7 @@ namespace BlueLaminate.EFCore.Migrations .IsUnique() .HasDatabaseName("ix_steam_users_steam_id"); - b.ToTable("steam_users", (string)null); + b.ToTable("steam_users", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.Trade", b => @@ -329,7 +330,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("ToUserId") .HasDatabaseName("ix_trades_to_user_id"); - b.ToTable("trades", (string)null); + b.ToTable("trades", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.TradeItem", b => @@ -358,7 +359,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasIndex("TradeId") .HasDatabaseName("ix_trade_items_trade_id"); - b.ToTable("trade_items", (string)null); + b.ToTable("trade_items", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.Weapon", b => @@ -388,7 +389,7 @@ namespace BlueLaminate.EFCore.Migrations b.HasKey("Id") .HasName("pk_weapons"); - b.ToTable("weapons", (string)null); + b.ToTable("weapons", "skintracker"); }); modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b => diff --git a/BlueLaminate/BlueLaminate.EFCore/appsettings.json b/BlueLaminate/BlueLaminate.EFCore/appsettings.json new file mode 100644 index 0000000..ef45fd6 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "SkinTracker": "Host=localhost;Port=5432;Database=skintracker;Username=postgres" + } +} diff --git a/db/00_drop_legacy_role.sql b/db/00_drop_legacy_role.sql new file mode 100644 index 0000000..083c0e7 --- /dev/null +++ b/db/00_drop_legacy_role.sql @@ -0,0 +1,40 @@ +-- ============================================================ +-- CS2 Skin Tracker — drop a pre-existing skintracker_app role +-- Run ONCE as a superuser (e.g. postgres) BEFORE +-- 01_schema_and_roles.sql, to remove an older role and +-- everything it owns so the creation script starts clean. +-- +-- IMPORTANT: +-- * Connect to the SAME database that contains the old +-- skintracker schema/tables. DROP OWNED only affects the +-- current database, so if the role owns objects in more +-- than one database, run this in each of them. +-- * This is DESTRUCTIVE: it drops the schema, tables, and any +-- data the role owns. Back up first if you need the data. +-- ============================================================ + +-- 1. (Optional) Terminate any live sessions still using the role, +-- otherwise the DROP ROLE can fail with "role is being used". +SELECT pg_terminate_backend(pid) +FROM pg_stat_activity +WHERE usename = 'skintracker_app' + AND pid <> pg_backend_pid(); + +-- 2. Reassign ownership of the DATABASE away from the role. +-- DROP OWNED BY does NOT cover database ownership (a database is a +-- cluster-level object), so the role must hand off the database +-- first or DROP ROLE fails with "owner of database skintracker". +-- Run this while connected as the target owner (e.g. postgres). +ALTER DATABASE skintracker OWNER TO CURRENT_USER; + +-- 3. Drop everything the role owns (schema, tables, indexes, etc.) +-- and revoke any privileges granted to it, then drop the role. +-- Guarded so the script is safe to run even if the role is gone. +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'skintracker_app') THEN + EXECUTE 'DROP OWNED BY skintracker_app CASCADE'; + EXECUTE 'DROP ROLE skintracker_app'; + END IF; +END +$$; diff --git a/db/01_schema_and_roles.sql b/db/01_schema_and_roles.sql new file mode 100644 index 0000000..c01c240 --- /dev/null +++ b/db/01_schema_and_roles.sql @@ -0,0 +1,40 @@ +-- ============================================================ +-- CS2 Skin Tracker — schema & role hardening +-- Run ONCE as a superuser (e.g. postgres), connected to the +-- skintracker database, before the app's first migration. +-- +-- Replace the password placeholders before running. +-- The application/migration connection should authenticate as +-- the skintracker_app role. +-- ============================================================ + +-- 1. Application login role (least-privilege; not a superuser). +-- Skip the CREATE if the role already exists. +CREATE ROLE skintracker_app WITH LOGIN PASSWORD 'change-me-strong-password'; + +-- 2. Create the schema and make the app role its owner. +-- Because the app owns it, EF's `EnsureSchema` (CREATE SCHEMA IF +-- NOT EXISTS) becomes a no-op and the app can create/alter tables +-- here during `database update` without any rights on `public`. +CREATE SCHEMA IF NOT EXISTS skintracker AUTHORIZATION skintracker_app; + +-- 3. Lock down the default `public` schema. +-- Historically every role had CREATE on public; revoke it so no +-- objects can be created there by accident. (PG15+ already removed +-- this by default, but being explicit is harmless and portable.) +REVOKE CREATE ON SCHEMA public FROM PUBLIC; + +-- 4. Make the app role use its own schema by default (so unqualified +-- object names resolve to skintracker, not public). +ALTER ROLE skintracker_app SET search_path = skintracker; + +-- ------------------------------------------------------------ +-- Optional: a read-only role for reporting / BI. +-- Uncomment if you need separate read-only access. +-- ------------------------------------------------------------ +-- CREATE ROLE skintracker_readonly WITH LOGIN PASSWORD 'change-me-too'; +-- GRANT USAGE ON SCHEMA skintracker TO skintracker_readonly; +-- GRANT SELECT ON ALL TABLES IN SCHEMA skintracker TO skintracker_readonly; +-- -- Apply automatically to tables created LATER by the app role: +-- ALTER DEFAULT PRIVILEGES FOR ROLE skintracker_app IN SCHEMA skintracker +-- GRANT SELECT ON TABLES TO skintracker_readonly;