feat: establish auth listing and image vertical slices

Includes MFA-capable authentication, session and CSRF security, password recovery, listing lifecycle and ownership rules, inventory image metadata, local ImageStorage, migrations, tests, and implementation status documentation. Verification: npm test (62 passing) and integration suite (14 passing).
This commit is contained in:
2026-09-08 17:32:03 -04:00
commit c728016dec
128 changed files with 9788 additions and 0 deletions
View File
@@ -0,0 +1,87 @@
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE TYPE "user_role" AS ENUM ('ADMIN');
CREATE TYPE "inventory_status" AS ENUM ('ACTIVE', 'SOLD', 'ARCHIVED');
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"email" text NOT NULL,
"password_hash" text NOT NULL,
"role" "user_role" DEFAULT 'ADMIN' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
CREATE UNIQUE INDEX "users_email_unique" ON "users" ("email");
CREATE TABLE "sellers" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL REFERENCES "users"("id"),
"display_name" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
CREATE UNIQUE INDEX "sellers_user_id_unique" ON "sellers" ("user_id");
CREATE TABLE "categories" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"slug" text NOT NULL,
"active" boolean DEFAULT true NOT NULL,
"sort" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
CREATE UNIQUE INDEX "categories_name_unique" ON "categories" ("name");
CREATE UNIQUE INDEX "categories_slug_unique" ON "categories" ("slug");
CREATE TABLE "condition_profiles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"category_id" uuid NOT NULL REFERENCES "categories"("id"),
"name" text NOT NULL,
"active" boolean DEFAULT true NOT NULL,
"sort" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "conditions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"condition_profile_id" uuid NOT NULL REFERENCES "condition_profiles"("id"),
"name" text NOT NULL,
"code" text NOT NULL,
"quality_rank" integer NOT NULL,
"description" text,
"active" boolean DEFAULT true NOT NULL,
"sort" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "conditions_profile_code_unique" UNIQUE("condition_profile_id", "code")
);
CREATE TABLE "products" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"seller_id" uuid NOT NULL REFERENCES "sellers"("id"),
"category_id" uuid NOT NULL REFERENCES "categories"("id"),
"name" text NOT NULL,
"slug" text NOT NULL,
"description" text,
"brand" text,
"set_name" text,
"card_number" text,
"event_name" text,
"release_year" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "products_release_year_valid" CHECK ("release_year" IS NULL OR "release_year" BETWEEN 1996 AND 9999),
CONSTRAINT "products_seller_slug_unique" UNIQUE("seller_id", "slug")
);
CREATE TABLE "inventory_items" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"product_id" uuid NOT NULL REFERENCES "products"("id"),
"condition_id" uuid NOT NULL REFERENCES "conditions"("id"),
"status" "inventory_status" DEFAULT 'ACTIVE' NOT NULL,
"notes" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
@@ -0,0 +1,14 @@
CREATE TABLE "sessions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL REFERENCES "users"("id"),
"session_token_hash" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"last_activity_at" timestamp with time zone DEFAULT now() NOT NULL,
"ip_address" text,
"user_agent" text,
"revoked_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "sessions_token_hash_unique" UNIQUE("session_token_hash")
);
CREATE INDEX "sessions_user_id_index" ON "sessions" ("user_id");
CREATE INDEX "sessions_expires_at_index" ON "sessions" ("expires_at");
@@ -0,0 +1,5 @@
ALTER TABLE "sessions" ADD COLUMN "csrf_token_hash" text;
UPDATE "sessions" SET "csrf_token_hash" = '' WHERE "csrf_token_hash" IS NULL;
ALTER TABLE "sessions" ALTER COLUMN "csrf_token_hash" SET NOT NULL;
@@ -0,0 +1,11 @@
CREATE TABLE "security_audit_events" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid REFERENCES "users"("id"),
"event" text NOT NULL,
"ip_address" text,
"user_agent" text,
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
CREATE INDEX "security_audit_events_user_id_index" ON "security_audit_events" ("user_id");
CREATE INDEX "security_audit_events_event_created_at_index" ON "security_audit_events" ("event", "created_at");
@@ -0,0 +1,10 @@
CREATE TABLE "password_reset_tokens" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL REFERENCES "users"("id"),
"token_hash" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"used_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "password_reset_tokens_token_hash_unique" UNIQUE("token_hash")
);
CREATE INDEX "password_reset_tokens_user_id_index" ON "password_reset_tokens" ("user_id");
+12
View File
@@ -0,0 +1,12 @@
CREATE TABLE "mfa_methods" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL REFERENCES "users"("id"),
"type" text NOT NULL,
"secret_ciphertext" text NOT NULL,
"name" text NOT NULL,
"is_enabled" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"last_used_at" timestamp with time zone,
"disabled_at" timestamp with time zone
);
CREATE INDEX "mfa_methods_user_id_index" ON "mfa_methods" ("user_id");
+17
View File
@@ -0,0 +1,17 @@
CREATE TYPE "listing_status" AS ENUM ('DRAFT', 'AVAILABLE', 'PENDING', 'CLOSED', 'ARCHIVED');
CREATE TABLE "listings" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"inventory_item_id" uuid NOT NULL REFERENCES "inventory_items"("id"),
"product_name" text NOT NULL,
"category_id" uuid NOT NULL,
"condition_id" uuid NOT NULL REFERENCES "conditions"("id"),
"asking_price_minor" integer NOT NULL,
"status" "listing_status" DEFAULT 'DRAFT' NOT NULL,
"notes" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
CREATE INDEX "listings_inventory_item_id_index" ON "listings" ("inventory_item_id");
CREATE INDEX "listings_status_index" ON "listings" ("status");
@@ -0,0 +1 @@
ALTER TABLE "listings" ADD COLUMN "image_count" integer NOT NULL DEFAULT 1;
@@ -0,0 +1,18 @@
CREATE TABLE "inventory_images" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"inventory_item_id" uuid NOT NULL REFERENCES "inventory_items"("id"),
"storage_key" text NOT NULL,
"original_filename" text NOT NULL,
"mime_type" text NOT NULL,
"file_size" integer NOT NULL,
"width" integer NOT NULL,
"height" integer NOT NULL,
"alt_text" text,
"sort_order" integer DEFAULT 0 NOT NULL,
"is_primary" boolean DEFAULT false NOT NULL,
"processing_status" text DEFAULT 'PENDING' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
CREATE INDEX "inventory_images_inventory_item_id_index" ON "inventory_images" ("inventory_item_id");
+69
View File
@@ -0,0 +1,69 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1788710400000,
"tag": "0000_core_inventory",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1788768000000,
"tag": "0001_auth_sessions",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1788854400000,
"tag": "0002_session_csrf_tokens",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1788940800000,
"tag": "0003_security_audit_events",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1789027200000,
"tag": "0004_password_reset_tokens",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1789113600000,
"tag": "0005_mfa_methods",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1789200000000,
"tag": "0006_listings",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1789286400000,
"tag": "0007_listing_image_count",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1789372800000,
"tag": "0008_inventory_images",
"breakpoints": true
}
]
}
View File