feat(core): scaffold account manager platform
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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`),
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "drizzle.config.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user