diff --git a/packages/db/package.json b/packages/db/package.json index c109ced..0381152 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -14,12 +14,14 @@ "db:generate": "drizzle-kit generate", "db:studio": "drizzle-kit studio", "db:migrate": "drizzle-kit migrate", + "db:seed": "bun --env-file=../../apps/server/.env run ./src/seed.ts", "db:start": "docker compose up -d", "db:watch": "docker compose up", "db:stop": "docker compose stop", "db:down": "docker compose down" }, "dependencies": { + "better-auth": "catalog:", "@minmon/env": "workspace:*", "dotenv": "catalog:", "drizzle-orm": "^0.45.1", diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 7d012d4..76299b2 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -3,6 +3,8 @@ import { drizzle } from "drizzle-orm/node-postgres"; import * as schema from "./schema"; +export * from "./schema"; + export function createDb() { return drizzle(env.DATABASE_URL, { schema }); } diff --git a/packages/db/src/migrations/0000_thick_molecule_man.sql b/packages/db/src/migrations/0000_thick_molecule_man.sql new file mode 100644 index 0000000..71ecfc9 --- /dev/null +++ b/packages/db/src/migrations/0000_thick_molecule_man.sql @@ -0,0 +1,136 @@ +CREATE TYPE "public"."domain_resolution_status" AS ENUM('pending', 'resolved', 'mismatch', 'unreachable');--> statement-breakpoint +CREATE TYPE "public"."note_category" AS ENUM('general', 'maintenance', 'incident', 'todo');--> statement-breakpoint +CREATE TYPE "public"."server_environment" AS ENUM('development', 'staging', 'production');--> statement-breakpoint +CREATE TYPE "public"."server_status" AS ENUM('healthy', 'warning', 'critical', 'offline');--> statement-breakpoint +CREATE TYPE "public"."service_status" AS ENUM('running', 'degraded', 'stopped', 'maintenance');--> statement-breakpoint +CREATE TYPE "public"."service_type" AS ENUM('frontend', 'backend', 'database', 'worker', 'proxy', 'monitoring', 'other');--> statement-breakpoint +CREATE TABLE "account" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "session" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + CONSTRAINT "session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "image" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "user_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "domain" ( + "id" text PRIMARY KEY NOT NULL, + "server_id" text NOT NULL, + "service_id" text, + "name" text NOT NULL, + "expected_server_ip" text NOT NULL, + "last_resolved_ipv4" text, + "last_resolved_ipv6" text, + "resolution_status" "domain_resolution_status" DEFAULT 'pending' NOT NULL, + "last_checked_at" timestamp, + "remarks" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "note" ( + "id" text PRIMARY KEY NOT NULL, + "server_id" text, + "service_id" text, + "domain_id" text, + "title" text NOT NULL, + "content" text NOT NULL, + "category" "note_category" DEFAULT 'general' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "server" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "host" text NOT NULL, + "ip_address" text NOT NULL, + "provider" text, + "region" text, + "environment" "server_environment" DEFAULT 'production' NOT NULL, + "status" "server_status" DEFAULT 'healthy' NOT NULL, + "operating_system" text, + "remarks" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "service" ( + "id" text PRIMARY KEY NOT NULL, + "server_id" text NOT NULL, + "name" text NOT NULL, + "type" "service_type" DEFAULT 'other' NOT NULL, + "status" "service_status" DEFAULT 'running' NOT NULL, + "port" text, + "version" text, + "health_check_path" text, + "remarks" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "domain" ADD CONSTRAINT "domain_server_id_server_id_fk" FOREIGN KEY ("server_id") REFERENCES "public"."server"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "domain" ADD CONSTRAINT "domain_service_id_service_id_fk" FOREIGN KEY ("service_id") REFERENCES "public"."service"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "note" ADD CONSTRAINT "note_server_id_server_id_fk" FOREIGN KEY ("server_id") REFERENCES "public"."server"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "note" ADD CONSTRAINT "note_service_id_service_id_fk" FOREIGN KEY ("service_id") REFERENCES "public"."service"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "note" ADD CONSTRAINT "note_domain_id_domain_id_fk" FOREIGN KEY ("domain_id") REFERENCES "public"."domain"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "service" ADD CONSTRAINT "service_server_id_server_id_fk" FOREIGN KEY ("server_id") REFERENCES "public"."server"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "session_userId_idx" ON "session" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier");--> statement-breakpoint +CREATE UNIQUE INDEX "domain_name_unique" ON "domain" USING btree ("name");--> statement-breakpoint +CREATE INDEX "domain_serverId_idx" ON "domain" USING btree ("server_id");--> statement-breakpoint +CREATE INDEX "domain_serviceId_idx" ON "domain" USING btree ("service_id");--> statement-breakpoint +CREATE INDEX "domain_resolutionStatus_idx" ON "domain" USING btree ("resolution_status");--> statement-breakpoint +CREATE INDEX "note_serverId_idx" ON "note" USING btree ("server_id");--> statement-breakpoint +CREATE INDEX "note_serviceId_idx" ON "note" USING btree ("service_id");--> statement-breakpoint +CREATE INDEX "note_domainId_idx" ON "note" USING btree ("domain_id");--> statement-breakpoint +CREATE INDEX "note_category_idx" ON "note" USING btree ("category");--> statement-breakpoint +CREATE UNIQUE INDEX "server_host_unique" ON "server" USING btree ("host");--> statement-breakpoint +CREATE UNIQUE INDEX "server_ipAddress_unique" ON "server" USING btree ("ip_address");--> statement-breakpoint +CREATE INDEX "server_environment_idx" ON "server" USING btree ("environment");--> statement-breakpoint +CREATE INDEX "server_status_idx" ON "server" USING btree ("status");--> statement-breakpoint +CREATE INDEX "service_serverId_idx" ON "service" USING btree ("server_id");--> statement-breakpoint +CREATE INDEX "service_status_idx" ON "service" USING btree ("status");--> statement-breakpoint +CREATE UNIQUE INDEX "service_serverId_name_unique" ON "service" USING btree ("server_id","name"); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0000_snapshot.json b/packages/db/src/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..055c6b0 --- /dev/null +++ b/packages/db/src/migrations/meta/0000_snapshot.json @@ -0,0 +1,1089 @@ +{ + "id": "86aed547-2dfd-44fb-a831-d9c72fec17fb", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_server_ip": { + "name": "expected_server_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_resolved_ipv4": { + "name": "last_resolved_ipv4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_resolved_ipv6": { + "name": "last_resolved_ipv6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_status": { + "name": "resolution_status", + "type": "domain_resolution_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "remarks": { + "name": "remarks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "domain_name_unique": { + "name": "domain_name_unique", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_serverId_idx": { + "name": "domain_serverId_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_serviceId_idx": { + "name": "domain_serviceId_idx", + "columns": [ + { + "expression": "service_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_resolutionStatus_idx": { + "name": "domain_resolutionStatus_idx", + "columns": [ + { + "expression": "resolution_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "domain_server_id_server_id_fk": { + "name": "domain_server_id_server_id_fk", + "tableFrom": "domain", + "tableTo": "server", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_service_id_service_id_fk": { + "name": "domain_service_id_service_id_fk", + "tableFrom": "domain", + "tableTo": "service", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.note": { + "name": "note", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "note_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "note_serverId_idx": { + "name": "note_serverId_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_serviceId_idx": { + "name": "note_serviceId_idx", + "columns": [ + { + "expression": "service_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_domainId_idx": { + "name": "note_domainId_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "note_category_idx": { + "name": "note_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "note_server_id_server_id_fk": { + "name": "note_server_id_server_id_fk", + "tableFrom": "note", + "tableTo": "server", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "note_service_id_service_id_fk": { + "name": "note_service_id_service_id_fk", + "tableFrom": "note", + "tableTo": "service", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "note_domain_id_domain_id_fk": { + "name": "note_domain_id_domain_id_fk", + "tableFrom": "note", + "tableTo": "domain", + "columnsFrom": [ + "domain_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "server_environment", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'production'" + }, + "status": { + "name": "status", + "type": "server_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'healthy'" + }, + "operating_system": { + "name": "operating_system", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remarks": { + "name": "remarks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "server_host_unique": { + "name": "server_host_unique", + "columns": [ + { + "expression": "host", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "server_ipAddress_unique": { + "name": "server_ipAddress_unique", + "columns": [ + { + "expression": "ip_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "server_environment_idx": { + "name": "server_environment_idx", + "columns": [ + { + "expression": "environment", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "server_status_idx": { + "name": "server_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.service": { + "name": "service", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "service_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "status": { + "name": "status", + "type": "service_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "port": { + "name": "port", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_check_path": { + "name": "health_check_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remarks": { + "name": "remarks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "service_serverId_idx": { + "name": "service_serverId_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "service_status_idx": { + "name": "service_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "service_serverId_name_unique": { + "name": "service_serverId_name_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "service_server_id_server_id_fk": { + "name": "service_server_id_server_id_fk", + "tableFrom": "service", + "tableTo": "server", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.domain_resolution_status": { + "name": "domain_resolution_status", + "schema": "public", + "values": [ + "pending", + "resolved", + "mismatch", + "unreachable" + ] + }, + "public.note_category": { + "name": "note_category", + "schema": "public", + "values": [ + "general", + "maintenance", + "incident", + "todo" + ] + }, + "public.server_environment": { + "name": "server_environment", + "schema": "public", + "values": [ + "development", + "staging", + "production" + ] + }, + "public.server_status": { + "name": "server_status", + "schema": "public", + "values": [ + "healthy", + "warning", + "critical", + "offline" + ] + }, + "public.service_status": { + "name": "service_status", + "schema": "public", + "values": [ + "running", + "degraded", + "stopped", + "maintenance" + ] + }, + "public.service_type": { + "name": "service_type", + "schema": "public", + "values": [ + "frontend", + "backend", + "database", + "worker", + "proxy", + "monitoring", + "other" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json new file mode 100644 index 0000000..f66548c --- /dev/null +++ b/packages/db/src/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1777458791180, + "tag": "0000_thick_molecule_man", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index d9d31ea..160fbf4 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -1,2 +1,3 @@ export * from "./auth"; +export * from "./monitoring"; export {}; diff --git a/packages/db/src/schema/monitoring.ts b/packages/db/src/schema/monitoring.ts new file mode 100644 index 0000000..2cf8b03 --- /dev/null +++ b/packages/db/src/schema/monitoring.ts @@ -0,0 +1,168 @@ +import { relations, sql } from "drizzle-orm"; +import { + index, + integer, + jsonb, + pgEnum, + pgTable, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core"; + +export const serverStatusEnum = pgEnum("server_status", ["active", "maintenance", "inactive"]); + +export const serviceTypeEnum = pgEnum("service_type", [ + "web_app", + "mail", + "database", + "api", + "proxy", + "other", +]); + +export const serviceProtocolEnum = pgEnum("service_protocol", [ + "http", + "https", + "tcp", + "udp", + "other", +]); + +export const serviceStatusEnum = pgEnum("service_status", ["active", "down", "maintenance"]); + +export const domainResolutionStatusEnum = pgEnum("domain_resolution_status", [ + "MATCH", + "MISMATCH", + "UNRESOLVED", +]); + +export const server = pgTable( + "server", + { + id: text("id").primaryKey(), + name: text("name").notNull(), + primaryIpAddress: text("primary_ip_address").notNull(), + secondaryIpAddress: text("secondary_ip_address"), + operatingSystem: text("operating_system"), + location: text("location"), + provider: text("provider"), + description: text("description"), + status: serverStatusEnum("status").default("active").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [ + uniqueIndex("server_name_unique").on(table.name), + index("server_status_idx").on(table.status), + ], +); + +export const service = pgTable( + "service", + { + id: text("id").primaryKey(), + serverId: text("server_id") + .notNull() + .references(() => server.id, { onDelete: "cascade" }), + name: text("name").notNull(), + type: serviceTypeEnum("type").default("other").notNull(), + internalPort: integer("internal_port"), + externalPort: integer("external_port"), + protocol: serviceProtocolEnum("protocol").default("http").notNull(), + status: serviceStatusEnum("status").default("active").notNull(), + description: text("description"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [ + index("service_server_id_idx").on(table.serverId), + index("service_status_idx").on(table.status), + uniqueIndex("service_server_name_unique").on(table.serverId, table.name), + ], +); + +export const domain = pgTable( + "domain", + { + id: text("id").primaryKey(), + serviceId: text("service_id") + .notNull() + .references(() => service.id, { onDelete: "cascade" }), + name: text("name").notNull(), + expectedServerIp: text("expected_server_ip").notNull(), + lastResolvedIp: text("last_resolved_ip"), + lastResolvedIps: jsonb("last_resolved_ips") + .$type() + .default(sql`'[]'::jsonb`) + .notNull(), + resolutionStatus: domainResolutionStatusEnum("resolution_status") + .default("UNRESOLVED") + .notNull(), + lastCheckedAt: timestamp("last_checked_at"), + remarks: text("remarks"), + lastCheckMessage: text("last_check_message"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [ + uniqueIndex("domain_name_unique").on(table.name), + index("domain_service_id_idx").on(table.serviceId), + index("domain_resolution_status_idx").on(table.resolutionStatus), + ], +); + +export const note = pgTable( + "note", + { + id: text("id").primaryKey(), + serverId: text("server_id") + .notNull() + .references(() => server.id, { onDelete: "cascade" }), + title: text("title").notNull(), + content: text("content").notNull(), + category: text("category"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("note_server_id_idx").on(table.serverId), index("note_category_idx").on(table.category)], +); + +export const serverRelations = relations(server, ({ many }) => ({ + services: many(service), + notes: many(note), +})); + +export const serviceRelations = relations(service, ({ one, many }) => ({ + server: one(server, { + fields: [service.serverId], + references: [server.id], + }), + domains: many(domain), +})); + +export const domainRelations = relations(domain, ({ one }) => ({ + service: one(service, { + fields: [domain.serviceId], + references: [service.id], + }), +})); + +export const noteRelations = relations(note, ({ one }) => ({ + server: one(server, { + fields: [note.serverId], + references: [server.id], + }), +})); diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts new file mode 100644 index 0000000..d7aa6d8 --- /dev/null +++ b/packages/db/src/seed.ts @@ -0,0 +1,278 @@ +import dotenv from "dotenv"; +import { hashPassword } from "better-auth/crypto"; + +dotenv.config({ + path: new URL("../../../apps/server/.env", import.meta.url).pathname, +}); + +import { db } from "./index"; +import { account, user } from "./schema/auth"; +import { domain, note, server, service } from "./schema/monitoring"; + +const now = new Date(); +const earlierToday = new Date(now.getTime() - 45 * 60 * 1000); +const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); + +const adminUser = { + id: "user_admin_minmon", + name: "Minmon Admin", + email: "admin@minmon.local", + password: "admin12345", +}; + +const servers = [ + { + id: "server_prod_1", + name: "Production VPS", + primaryIpAddress: "203.0.113.10", + secondaryIpAddress: "203.0.113.11", + operatingSystem: "Ubuntu 24.04 LTS", + location: "Singapore", + provider: "DigitalOcean", + description: "Main production server hosting the dashboard web app, API, and reverse proxy.", + status: "active" as const, + }, + { + id: "server_mail_1", + name: "Mail Server", + primaryIpAddress: "198.51.100.20", + secondaryIpAddress: null, + operatingSystem: "Debian 12", + location: "Jakarta", + provider: "Hetzner", + description: "Handles the demo mail service and SMTP/IMAP examples.", + status: "maintenance" as const, + }, + { + id: "server_lab_1", + name: "Campus Lab VM", + primaryIpAddress: "192.0.2.18", + secondaryIpAddress: null, + operatingSystem: "Ubuntu 22.04 LTS", + location: "Campus Lab", + provider: "University Lab", + description: "Used for student practice and lecturer demonstration in class.", + status: "inactive" as const, + }, +]; + +const services = [ + { + id: "service_dashboard_web", + serverId: "server_prod_1", + name: "Dashboard Web", + type: "web_app" as const, + internalPort: 3000, + externalPort: 443, + protocol: "https" as const, + status: "active" as const, + description: "Frontend application for the private server management dashboard.", + }, + { + id: "service_dashboard_api", + serverId: "server_prod_1", + name: "Dashboard API", + type: "api" as const, + internalPort: 3001, + externalPort: 443, + protocol: "https" as const, + status: "active" as const, + description: "Handles CRUD data, dashboard summary, and DNS checks.", + }, + { + id: "service_mail_gateway", + serverId: "server_mail_1", + name: "Mail Gateway", + type: "mail" as const, + internalPort: 25, + externalPort: 25, + protocol: "tcp" as const, + status: "maintenance" as const, + description: "Mail service used as a sample SMTP gateway for the assignment.", + }, + { + id: "service_lab_proxy", + serverId: "server_lab_1", + name: "Lab Reverse Proxy", + type: "proxy" as const, + internalPort: 80, + externalPort: 80, + protocol: "http" as const, + status: "down" as const, + description: "Reverse proxy for local practice deployments in the campus lab.", + }, +]; + +const domains = [ + { + id: "domain_dashboard_web", + serviceId: "service_dashboard_web", + name: "dashboard.minmon.dev", + expectedServerIp: "203.0.113.10", + lastResolvedIp: "203.0.113.10", + lastResolvedIps: ["203.0.113.10"], + resolutionStatus: "MATCH" as const, + lastCheckedAt: earlierToday, + remarks: "Primary domain for the dashboard frontend.", + lastCheckMessage: null, + }, + { + id: "domain_dashboard_api", + serviceId: "service_dashboard_api", + name: "api.minmon.dev", + expectedServerIp: "203.0.113.10", + lastResolvedIp: "203.0.113.15", + lastResolvedIps: ["203.0.113.15", "203.0.113.10"], + resolutionStatus: "MATCH" as const, + lastCheckedAt: earlierToday, + remarks: "API subdomain. Multiple A records still include the expected IP.", + lastCheckMessage: null, + }, + { + id: "domain_mail_gateway", + serviceId: "service_mail_gateway", + name: "mail.minmon.dev", + expectedServerIp: "198.51.100.20", + lastResolvedIp: "198.51.100.77", + lastResolvedIps: ["198.51.100.77"], + resolutionStatus: "MISMATCH" as const, + lastCheckedAt: yesterday, + remarks: "DNS still points to the old mail gateway.", + lastCheckMessage: null, + }, + { + id: "domain_lab_demo", + serviceId: "service_lab_proxy", + name: "lab.minmon.dev", + expectedServerIp: "192.0.2.18", + lastResolvedIp: null, + lastResolvedIps: [], + resolutionStatus: "UNRESOLVED" as const, + lastCheckedAt: yesterday, + remarks: "Lab VM is powered off outside teaching hours.", + lastCheckMessage: "DNS lookup failed because the host could not be resolved.", + }, +]; + +const notes = [ + { + id: "note_prod_backup", + serverId: "server_prod_1", + title: "Weekly backup reminder", + content: "Verify the PostgreSQL backup every Friday before the lecturer demo session.", + category: "maintenance", + }, + { + id: "note_prod_dataset", + serverId: "server_prod_1", + title: "Main sample dataset", + content: + "This production server and its services form the main student-facing sample dataset for CRUD and monitoring demos.", + category: "general", + }, + { + id: "note_mail_dns", + serverId: "server_mail_1", + title: "Update stale A record", + content: "Point mail.minmon.dev to the current mail server before final presentation week.", + category: "todo", + }, + { + id: "note_lab_status", + serverId: "server_lab_1", + title: "Lab VM inactive", + content: "The campus lab VM is intentionally inactive outside class hours, so related checks may fail.", + category: "incident", + }, +]; + +async function seedAuthAdmin() { + const existingUser = await db.query.user.findFirst({ + where: (table, { eq }) => eq(table.email, adminUser.email), + }); + + if (existingUser) { + return existingUser.id; + } + + const passwordHash = await hashPassword(adminUser.password); + + await db.insert(user).values({ + id: adminUser.id, + name: adminUser.name, + email: adminUser.email, + emailVerified: true, + createdAt: now, + updatedAt: now, + }); + + await db.insert(account).values({ + id: "account_admin_minmon", + accountId: adminUser.id, + providerId: "credential", + userId: adminUser.id, + password: passwordHash, + createdAt: now, + updatedAt: now, + }); + + return adminUser.id; +} + +async function seedMonitoringData() { + await db.delete(note); + await db.delete(domain); + await db.delete(service); + await db.delete(server); + + await db.insert(server).values( + servers.map((item) => ({ + ...item, + secondaryIpAddress: item.secondaryIpAddress ?? undefined, + createdAt: now, + updatedAt: now, + })), + ); + + await db.insert(service).values( + services.map((item) => ({ + ...item, + createdAt: now, + updatedAt: now, + })), + ); + + await db.insert(domain).values( + domains.map((item) => ({ + ...item, + lastResolvedIp: item.lastResolvedIp ?? undefined, + lastCheckMessage: item.lastCheckMessage ?? undefined, + createdAt: now, + updatedAt: now, + })), + ); + + await db.insert(note).values( + notes.map((item) => ({ + ...item, + createdAt: now, + updatedAt: now, + })), + ); +} + +async function seed() { + await seedAuthAdmin(); + await seedMonitoringData(); +} + +seed() + .then(() => { + console.info("Seeded auth admin and monitoring dashboard sample data."); + console.info("Admin login: admin@minmon.local / admin12345"); + process.exit(0); + }) + .catch((error) => { + console.error("Failed to seed dashboard sample data.", error); + process.exit(1); + });