feat(core): scaffold account manager platform

This commit is contained in:
dmg
2026-08-01 13:03:26 -04:00
commit 9d305e5dc9
36 changed files with 11898 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql",
schema: "./src/schema.ts",
out: "./drizzle",
dbCredentials: {
url: process.env.DATABASE_URL ?? "postgresql://minecraft:minecraft@localhost:5432/minecraft_accounts",
},
strict: true,
verbose: true,
});
@@ -0,0 +1,128 @@
CREATE TYPE "public"."ip_classification" AS ENUM('unknown', 'clear', 'vpn', 'proxy', 'hosting', 'tor');--> statement-breakpoint
CREATE TYPE "public"."ip_observation_source" AS ENUM('web', 'game');--> statement-breakpoint
CREATE TYPE "public"."minecraft_validation_status" AS ENUM('verified', 'user_confirmed');--> statement-breakpoint
CREATE TABLE "app_settings" (
"id" text PRIMARY KEY DEFAULT 'default' NOT NULL,
"discord_guild_id" text,
"registration_message" text DEFAULT 'Please register your Minecraft account before joining.' NOT NULL,
"created_at" timestamp (3) with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp (3) with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "events" (
"id" uuid PRIMARY KEY NOT NULL,
"spec_version" text DEFAULT '1.0' NOT NULL,
"source" text NOT NULL,
"type" text NOT NULL,
"subject" text,
"time" timestamp (3) with time zone NOT NULL,
"data_content_type" text DEFAULT 'application/json' NOT NULL,
"data_schema" text,
"data" jsonb NOT NULL,
"actor_user_id" uuid,
"ip_address" "inet",
"correlation_id" uuid,
"published_at" timestamp (3) with time zone,
"created_at" timestamp (3) with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ip_intelligence" (
"ip_address" "inet" PRIMARY KEY NOT NULL,
"classification" "ip_classification" DEFAULT 'unknown' NOT NULL,
"provider" text,
"raw_response" jsonb,
"checked_at" timestamp (3) with time zone,
"expires_at" timestamp (3) with time zone,
"created_at" timestamp (3) with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp (3) with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ip_observations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid,
"minecraft_account_id" uuid,
"source" "ip_observation_source" NOT NULL,
"ip_address" "inet" NOT NULL,
"minecraft_uuid" varchar(32),
"username" varchar(16),
"classification" "ip_classification" DEFAULT 'unknown' NOT NULL,
"observed_at" timestamp (3) with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "login_codes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"token_hash" text NOT NULL,
"discord_user_id" text NOT NULL,
"discord_username" text NOT NULL,
"discord_global_name" text,
"expires_at" timestamp (3) with time zone NOT NULL,
"consumed_at" timestamp (3) with time zone,
"created_at" timestamp (3) with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "minecraft_accounts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"minecraft_uuid" varchar(32),
"username" varchar(16) NOT NULL,
"validation_status" "minecraft_validation_status" NOT NULL,
"is_primary" boolean DEFAULT false NOT NULL,
"last_verified_at" timestamp (3) with time zone,
"deleted_at" timestamp (3) with time zone,
"created_at" timestamp (3) with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp (3) with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "plugin_credentials" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"server_id" text NOT NULL,
"secret_hash" text NOT NULL,
"revoked_at" timestamp (3) with time zone,
"created_at" timestamp (3) with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp (3) with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "sessions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"token_hash" text NOT NULL,
"expires_at" timestamp (3) with time zone NOT NULL,
"last_seen_at" timestamp (3) with time zone DEFAULT now() NOT NULL,
"revoked_at" timestamp (3) with time zone,
"created_at" timestamp (3) with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"discord_user_id" text NOT NULL,
"discord_username" text NOT NULL,
"discord_global_name" text,
"first_name" text,
"onboarding_completed_at" timestamp (3) with time zone,
"created_at" timestamp (3) with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp (3) with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "events" ADD CONSTRAINT "events_actor_user_id_users_id_fk" FOREIGN KEY ("actor_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ip_observations" ADD CONSTRAINT "ip_observations_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ip_observations" ADD CONSTRAINT "ip_observations_minecraft_account_id_minecraft_accounts_id_fk" FOREIGN KEY ("minecraft_account_id") REFERENCES "public"."minecraft_accounts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "minecraft_accounts" ADD CONSTRAINT "minecraft_accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "events_time_idx" ON "events" USING btree ("time");--> statement-breakpoint
CREATE INDEX "events_type_time_idx" ON "events" USING btree ("type","time");--> statement-breakpoint
CREATE INDEX "events_subject_time_idx" ON "events" USING btree ("subject","time");--> statement-breakpoint
CREATE INDEX "events_unpublished_idx" ON "events" USING btree ("created_at") WHERE "events"."published_at" is null;--> statement-breakpoint
CREATE INDEX "ip_observations_user_observed_idx" ON "ip_observations" USING btree ("user_id","observed_at");--> statement-breakpoint
CREATE INDEX "ip_observations_account_observed_idx" ON "ip_observations" USING btree ("minecraft_account_id","observed_at");--> statement-breakpoint
CREATE UNIQUE INDEX "login_codes_token_hash_uidx" ON "login_codes" USING btree ("token_hash");--> statement-breakpoint
CREATE INDEX "login_codes_discord_user_idx" ON "login_codes" USING btree ("discord_user_id");--> statement-breakpoint
CREATE INDEX "login_codes_expires_idx" ON "login_codes" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "minecraft_accounts_user_idx" ON "minecraft_accounts" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "minecraft_accounts_active_uuid_uidx" ON "minecraft_accounts" USING btree ("minecraft_uuid") WHERE "minecraft_accounts"."deleted_at" is null and "minecraft_accounts"."minecraft_uuid" is not null;--> statement-breakpoint
CREATE UNIQUE INDEX "minecraft_accounts_active_username_uidx" ON "minecraft_accounts" USING btree (lower("username")) WHERE "minecraft_accounts"."deleted_at" is null;--> statement-breakpoint
CREATE UNIQUE INDEX "minecraft_accounts_one_primary_per_user_uidx" ON "minecraft_accounts" USING btree ("user_id") WHERE "minecraft_accounts"."is_primary" = true and "minecraft_accounts"."deleted_at" is null;--> statement-breakpoint
CREATE UNIQUE INDEX "plugin_credentials_server_id_uidx" ON "plugin_credentials" USING btree ("server_id");--> statement-breakpoint
CREATE UNIQUE INDEX "sessions_token_hash_uidx" ON "sessions" USING btree ("token_hash");--> statement-breakpoint
CREATE INDEX "sessions_user_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "sessions_expires_idx" ON "sessions" USING btree ("expires_at");--> statement-breakpoint
CREATE UNIQUE INDEX "users_discord_user_id_uidx" ON "users" USING btree ("discord_user_id");
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1785603505066,
"tag": "0000_supreme_human_fly",
"breakpoints": true
}
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@minecraft-account-manager/database",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts",
"./schema": "./src/schema.ts"
},
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"drizzle-orm": "^0.45.1",
"postgres": "^3.4.8"
},
"devDependencies": {
"drizzle-kit": "^0.31.10",
"typescript": "^5.9.3"
}
}
+13
View File
@@ -0,0 +1,13 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
export function createDatabase(databaseUrl: string) {
const client = postgres(databaseUrl, { max: 10 });
return {
client,
db: drizzle(client, { schema }),
};
}
export * from "./schema";
+213
View File
@@ -0,0 +1,213 @@
import { sql } from "drizzle-orm";
import {
boolean,
index,
inet,
jsonb,
pgEnum,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
varchar,
} from "drizzle-orm/pg-core";
function timestamps() {
return {
createdAt: timestamp("created_at", { withTimezone: true, mode: "date", precision: 3 })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date", precision: 3 })
.notNull()
.defaultNow(),
};
}
function createdAt() {
return timestamp("created_at", { withTimezone: true, mode: "date", precision: 3 })
.notNull()
.defaultNow();
}
export const minecraftValidationStatus = pgEnum("minecraft_validation_status", [
"verified",
"user_confirmed",
]);
export const ipObservationSource = pgEnum("ip_observation_source", ["web", "game"]);
export const ipClassification = pgEnum("ip_classification", [
"unknown",
"clear",
"vpn",
"proxy",
"hosting",
"tor",
]);
export const users = pgTable(
"users",
{
id: uuid("id").primaryKey().defaultRandom(),
discordUserId: text("discord_user_id").notNull(),
discordUsername: text("discord_username").notNull(),
discordGlobalName: text("discord_global_name"),
firstName: text("first_name"),
onboardingCompletedAt: timestamp("onboarding_completed_at", {
withTimezone: true,
mode: "date",
precision: 3,
}),
...timestamps(),
},
(table) => [uniqueIndex("users_discord_user_id_uidx").on(table.discordUserId)],
);
export const minecraftAccounts = pgTable(
"minecraft_accounts",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
minecraftUuid: varchar("minecraft_uuid", { length: 32 }),
username: varchar("username", { length: 16 }).notNull(),
validationStatus: minecraftValidationStatus("validation_status").notNull(),
isPrimary: boolean("is_primary").notNull().default(false),
lastVerifiedAt: timestamp("last_verified_at", { withTimezone: true, mode: "date", precision: 3 }),
deletedAt: timestamp("deleted_at", { withTimezone: true, mode: "date", precision: 3 }),
...timestamps(),
},
(table) => [
index("minecraft_accounts_user_idx").on(table.userId),
uniqueIndex("minecraft_accounts_active_uuid_uidx")
.on(table.minecraftUuid)
.where(sql`${table.deletedAt} is null and ${table.minecraftUuid} is not null`),
uniqueIndex("minecraft_accounts_active_username_uidx")
.on(sql`lower(${table.username})`)
.where(sql`${table.deletedAt} is null`),
uniqueIndex("minecraft_accounts_one_primary_per_user_uidx")
.on(table.userId)
.where(sql`${table.isPrimary} = true and ${table.deletedAt} is null`),
],
);
export const loginCodes = pgTable(
"login_codes",
{
id: uuid("id").primaryKey().defaultRandom(),
tokenHash: text("token_hash").notNull(),
discordUserId: text("discord_user_id").notNull(),
discordUsername: text("discord_username").notNull(),
discordGlobalName: text("discord_global_name"),
expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date", precision: 3 }).notNull(),
consumedAt: timestamp("consumed_at", { withTimezone: true, mode: "date", precision: 3 }),
createdAt: createdAt(),
},
(table) => [
uniqueIndex("login_codes_token_hash_uidx").on(table.tokenHash),
index("login_codes_discord_user_idx").on(table.discordUserId),
index("login_codes_expires_idx").on(table.expiresAt),
],
);
export const sessions = pgTable(
"sessions",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
tokenHash: text("token_hash").notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date", precision: 3 }).notNull(),
lastSeenAt: timestamp("last_seen_at", { withTimezone: true, mode: "date", precision: 3 })
.notNull()
.defaultNow(),
revokedAt: timestamp("revoked_at", { withTimezone: true, mode: "date", precision: 3 }),
createdAt: createdAt(),
},
(table) => [
uniqueIndex("sessions_token_hash_uidx").on(table.tokenHash),
index("sessions_user_idx").on(table.userId),
index("sessions_expires_idx").on(table.expiresAt),
],
);
export const appSettings = pgTable("app_settings", {
id: text("id").primaryKey().default("default"),
discordGuildId: text("discord_guild_id"),
registrationMessage: text("registration_message")
.notNull()
.default("Please register your Minecraft account before joining."),
...timestamps(),
});
export const ipIntelligence = pgTable("ip_intelligence", {
ipAddress: inet("ip_address").primaryKey(),
classification: ipClassification("classification").notNull().default("unknown"),
provider: text("provider"),
rawResponse: jsonb("raw_response").$type<Record<string, unknown>>(),
checkedAt: timestamp("checked_at", { withTimezone: true, mode: "date", precision: 3 }),
expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date", precision: 3 }),
...timestamps(),
});
export const ipObservations = pgTable(
"ip_observations",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id").references(() => users.id, { onDelete: "set null" }),
minecraftAccountId: uuid("minecraft_account_id").references(() => minecraftAccounts.id, {
onDelete: "set null",
}),
source: ipObservationSource("source").notNull(),
ipAddress: inet("ip_address").notNull(),
minecraftUuid: varchar("minecraft_uuid", { length: 32 }),
username: varchar("username", { length: 16 }),
classification: ipClassification("classification").notNull().default("unknown"),
observedAt: timestamp("observed_at", { withTimezone: true, mode: "date", precision: 3 })
.notNull()
.defaultNow(),
},
(table) => [
index("ip_observations_user_observed_idx").on(table.userId, table.observedAt),
index("ip_observations_account_observed_idx").on(table.minecraftAccountId, table.observedAt),
],
);
export const pluginCredentials = pgTable(
"plugin_credentials",
{
id: uuid("id").primaryKey().defaultRandom(),
serverId: text("server_id").notNull(),
secretHash: text("secret_hash").notNull(),
revokedAt: timestamp("revoked_at", { withTimezone: true, mode: "date", precision: 3 }),
...timestamps(),
},
(table) => [uniqueIndex("plugin_credentials_server_id_uidx").on(table.serverId)],
);
export const events = pgTable(
"events",
{
id: uuid("id").primaryKey(),
specVersion: text("spec_version").notNull().default("1.0"),
source: text("source").notNull(),
type: text("type").notNull(),
subject: text("subject"),
time: timestamp("time", { withTimezone: true, mode: "date", precision: 3 }).notNull(),
dataContentType: text("data_content_type").notNull().default("application/json"),
dataSchema: text("data_schema"),
data: jsonb("data").$type<Record<string, unknown>>().notNull(),
actorUserId: uuid("actor_user_id").references(() => users.id, { onDelete: "set null" }),
ipAddress: inet("ip_address"),
correlationId: uuid("correlation_id"),
publishedAt: timestamp("published_at", { withTimezone: true, mode: "date", precision: 3 }),
createdAt: createdAt(),
},
(table) => [
index("events_time_idx").on(table.time),
index("events_type_time_idx").on(table.type, table.time),
index("events_subject_time_idx").on(table.subject, table.time),
index("events_unpublished_idx").on(table.createdAt).where(sql`${table.publishedAt} is null`),
],
);
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node"]
},
"include": ["src/**/*.ts", "drizzle.config.ts"]
}