Initial push
This commit is contained in:
102
lib/db/index.ts
Normal file
102
lib/db/index.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const DB_PATH = "./data/cubeadmin.db";
|
||||
|
||||
// Ensure the data directory exists before opening the database
|
||||
mkdirSync(dirname(DB_PATH), { recursive: true });
|
||||
|
||||
const sqlite = new Database(DB_PATH, { create: true });
|
||||
|
||||
// Enable WAL mode for better concurrent read performance
|
||||
sqlite.exec("PRAGMA journal_mode = WAL;");
|
||||
sqlite.exec("PRAGMA foreign_keys = ON;");
|
||||
|
||||
export const db = drizzle(sqlite, { schema });
|
||||
|
||||
export type DB = typeof db;
|
||||
|
||||
// Re-export all schema tables for convenient imports from a single location
|
||||
export {
|
||||
users,
|
||||
sessions,
|
||||
accounts,
|
||||
verifications,
|
||||
invitations,
|
||||
mcPlayers,
|
||||
playerBans,
|
||||
playerChatHistory,
|
||||
playerSpawnPoints,
|
||||
plugins,
|
||||
backups,
|
||||
scheduledTasks,
|
||||
auditLogs,
|
||||
serverSettings,
|
||||
} from "./schema";
|
||||
|
||||
// Re-export Zod schemas
|
||||
export {
|
||||
insertUserSchema,
|
||||
selectUserSchema,
|
||||
insertSessionSchema,
|
||||
selectSessionSchema,
|
||||
insertAccountSchema,
|
||||
selectAccountSchema,
|
||||
insertVerificationSchema,
|
||||
selectVerificationSchema,
|
||||
insertInvitationSchema,
|
||||
selectInvitationSchema,
|
||||
insertMcPlayerSchema,
|
||||
selectMcPlayerSchema,
|
||||
insertPlayerBanSchema,
|
||||
selectPlayerBanSchema,
|
||||
insertPlayerChatHistorySchema,
|
||||
selectPlayerChatHistorySchema,
|
||||
insertPlayerSpawnPointSchema,
|
||||
selectPlayerSpawnPointSchema,
|
||||
insertPluginSchema,
|
||||
selectPluginSchema,
|
||||
insertBackupSchema,
|
||||
selectBackupSchema,
|
||||
insertScheduledTaskSchema,
|
||||
selectScheduledTaskSchema,
|
||||
insertAuditLogSchema,
|
||||
selectAuditLogSchema,
|
||||
insertServerSettingsSchema,
|
||||
selectServerSettingsSchema,
|
||||
} from "./schema";
|
||||
|
||||
// Re-export inferred types
|
||||
export type {
|
||||
User,
|
||||
NewUser,
|
||||
Session,
|
||||
NewSession,
|
||||
Account,
|
||||
NewAccount,
|
||||
Verification,
|
||||
NewVerification,
|
||||
Invitation,
|
||||
NewInvitation,
|
||||
McPlayer,
|
||||
NewMcPlayer,
|
||||
PlayerBan,
|
||||
NewPlayerBan,
|
||||
PlayerChatHistory,
|
||||
NewPlayerChatHistory,
|
||||
PlayerSpawnPoint,
|
||||
NewPlayerSpawnPoint,
|
||||
Plugin,
|
||||
NewPlugin,
|
||||
Backup,
|
||||
NewBackup,
|
||||
ScheduledTask,
|
||||
NewScheduledTask,
|
||||
AuditLog,
|
||||
NewAuditLog,
|
||||
ServerSettings,
|
||||
NewServerSettings,
|
||||
} from "./schema";
|
||||
31
lib/db/migrate.ts
Normal file
31
lib/db/migrate.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { db } from "./index";
|
||||
|
||||
const MIGRATIONS_FOLDER = resolve(process.cwd(), "drizzle");
|
||||
|
||||
/**
|
||||
* Run all pending Drizzle migrations at startup.
|
||||
*
|
||||
* If the migrations folder does not exist yet (e.g. fresh clone before the
|
||||
* first `bun run db:generate` has been executed) the function exits silently
|
||||
* so that the application can still start in development without crashing.
|
||||
*/
|
||||
export function runMigrations(): void {
|
||||
if (!existsSync(MIGRATIONS_FOLDER)) {
|
||||
console.warn(
|
||||
`[migrate] Migrations folder not found at "${MIGRATIONS_FOLDER}". ` +
|
||||
"Skipping migrations — run `bun run db:generate` to create migration files.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
|
||||
console.log("[migrate] Database migrations applied successfully.");
|
||||
} catch (err) {
|
||||
console.error("[migrate] Failed to apply database migrations:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
359
lib/db/schema.ts
Normal file
359
lib/db/schema.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import {
|
||||
sqliteTable,
|
||||
text,
|
||||
integer,
|
||||
real,
|
||||
uniqueIndex,
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Better Auth core tables
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const users = sqliteTable("users", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: integer("email_verified", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
image: text("image"),
|
||||
role: text("role", {
|
||||
enum: ["superadmin", "admin", "moderator"],
|
||||
})
|
||||
.notNull()
|
||||
.default("moderator"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const sessions = sqliteTable("sessions", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
token: text("token").notNull().unique(),
|
||||
expiresAt: integer("expires_at").notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const accounts = sqliteTable("accounts", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
expiresAt: integer("expires_at"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const verifications = sqliteTable("verifications", {
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: integer("expires_at").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Invitation system
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const invitations = sqliteTable("invitations", {
|
||||
id: text("id").primaryKey(),
|
||||
email: text("email").notNull(),
|
||||
role: text("role", {
|
||||
enum: ["superadmin", "admin", "moderator"],
|
||||
})
|
||||
.notNull()
|
||||
.default("moderator"),
|
||||
invitedBy: text("invited_by")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
token: text("token").notNull().unique(),
|
||||
expiresAt: integer("expires_at").notNull(),
|
||||
acceptedAt: integer("accepted_at"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Minecraft player management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const mcPlayers = sqliteTable(
|
||||
"mc_players",
|
||||
{
|
||||
id: text("id").primaryKey(), // uuid
|
||||
uuid: text("uuid").notNull(), // Minecraft UUID
|
||||
username: text("username").notNull(),
|
||||
firstSeen: integer("first_seen"),
|
||||
lastSeen: integer("last_seen"),
|
||||
isOnline: integer("is_online", { mode: "boolean" }).notNull().default(false),
|
||||
playTime: integer("play_time").notNull().default(0), // minutes
|
||||
role: text("role"),
|
||||
isBanned: integer("is_banned", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
notes: text("notes"),
|
||||
},
|
||||
(table) => [uniqueIndex("mc_players_uuid_idx").on(table.uuid)],
|
||||
);
|
||||
|
||||
export const playerBans = sqliteTable("player_bans", {
|
||||
id: text("id").primaryKey(),
|
||||
playerId: text("player_id")
|
||||
.notNull()
|
||||
.references(() => mcPlayers.id, { onDelete: "cascade" }),
|
||||
reason: text("reason"),
|
||||
bannedBy: text("banned_by").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
bannedAt: integer("banned_at").notNull(),
|
||||
expiresAt: integer("expires_at"),
|
||||
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
|
||||
unbannedBy: text("unbanned_by").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
unbannedAt: integer("unbanned_at"),
|
||||
});
|
||||
|
||||
export const playerChatHistory = sqliteTable("player_chat_history", {
|
||||
id: text("id").primaryKey(),
|
||||
playerId: text("player_id")
|
||||
.notNull()
|
||||
.references(() => mcPlayers.id, { onDelete: "cascade" }),
|
||||
message: text("message").notNull(),
|
||||
channel: text("channel"),
|
||||
timestamp: integer("timestamp").notNull(),
|
||||
serverId: text("server_id"),
|
||||
});
|
||||
|
||||
export const playerSpawnPoints = sqliteTable("player_spawn_points", {
|
||||
id: text("id").primaryKey(),
|
||||
playerId: text("player_id")
|
||||
.notNull()
|
||||
.references(() => mcPlayers.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
world: text("world").notNull(),
|
||||
x: real("x").notNull(),
|
||||
y: real("y").notNull(),
|
||||
z: real("z").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const plugins = sqliteTable("plugins", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
version: text("version"),
|
||||
description: text("description"),
|
||||
isEnabled: integer("is_enabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true),
|
||||
jarFile: text("jar_file"),
|
||||
config: text("config"), // JSON blob
|
||||
installedAt: integer("installed_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Backup management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const backups = sqliteTable("backups", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
type: text("type", {
|
||||
enum: ["worlds", "plugins", "config", "full"],
|
||||
}).notNull(),
|
||||
size: integer("size"), // bytes
|
||||
path: text("path"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
status: text("status", {
|
||||
enum: ["pending", "running", "completed", "failed"],
|
||||
})
|
||||
.notNull()
|
||||
.default("pending"),
|
||||
triggeredBy: text("triggered_by").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scheduled tasks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const scheduledTasks = sqliteTable("scheduled_tasks", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
cronExpression: text("cron_expression").notNull(),
|
||||
command: text("command").notNull(), // MC command to run
|
||||
isEnabled: integer("is_enabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true),
|
||||
lastRun: integer("last_run"),
|
||||
nextRun: integer("next_run"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Audit logs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const auditLogs = sqliteTable("audit_logs", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").references(() => users.id, { onDelete: "set null" }),
|
||||
action: text("action").notNull(),
|
||||
target: text("target"),
|
||||
targetId: text("target_id"),
|
||||
details: text("details"), // JSON blob
|
||||
ipAddress: text("ip_address"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Server settings (singleton row, id = 1)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const serverSettings = sqliteTable("server_settings", {
|
||||
id: integer("id").primaryKey().default(1),
|
||||
minecraftPath: text("minecraft_path"),
|
||||
serverJar: text("server_jar"),
|
||||
serverVersion: text("server_version"),
|
||||
serverType: text("server_type", {
|
||||
enum: ["vanilla", "paper", "spigot", "bukkit", "fabric", "forge", "bedrock"],
|
||||
}),
|
||||
maxRam: integer("max_ram").default(4096),
|
||||
minRam: integer("min_ram").default(1024),
|
||||
rconEnabled: integer("rcon_enabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
rconPort: integer("rcon_port").default(25575),
|
||||
rconPassword: text("rcon_password"), // stored encrypted
|
||||
javaArgs: text("java_args"),
|
||||
autoStart: integer("auto_start", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
restartOnCrash: integer("restart_on_crash", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
backupEnabled: integer("backup_enabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
backupSchedule: text("backup_schedule"),
|
||||
bluemapEnabled: integer("bluemap_enabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
bluemapUrl: text("bluemap_url"),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Zod schemas (insert + select) for each table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const insertUserSchema = createInsertSchema(users);
|
||||
export const selectUserSchema = createSelectSchema(users);
|
||||
|
||||
export const insertSessionSchema = createInsertSchema(sessions);
|
||||
export const selectSessionSchema = createSelectSchema(sessions);
|
||||
|
||||
export const insertAccountSchema = createInsertSchema(accounts);
|
||||
export const selectAccountSchema = createSelectSchema(accounts);
|
||||
|
||||
export const insertVerificationSchema = createInsertSchema(verifications);
|
||||
export const selectVerificationSchema = createSelectSchema(verifications);
|
||||
|
||||
export const insertInvitationSchema = createInsertSchema(invitations);
|
||||
export const selectInvitationSchema = createSelectSchema(invitations);
|
||||
|
||||
export const insertMcPlayerSchema = createInsertSchema(mcPlayers);
|
||||
export const selectMcPlayerSchema = createSelectSchema(mcPlayers);
|
||||
|
||||
export const insertPlayerBanSchema = createInsertSchema(playerBans);
|
||||
export const selectPlayerBanSchema = createSelectSchema(playerBans);
|
||||
|
||||
export const insertPlayerChatHistorySchema =
|
||||
createInsertSchema(playerChatHistory);
|
||||
export const selectPlayerChatHistorySchema =
|
||||
createSelectSchema(playerChatHistory);
|
||||
|
||||
export const insertPlayerSpawnPointSchema =
|
||||
createInsertSchema(playerSpawnPoints);
|
||||
export const selectPlayerSpawnPointSchema =
|
||||
createSelectSchema(playerSpawnPoints);
|
||||
|
||||
export const insertPluginSchema = createInsertSchema(plugins);
|
||||
export const selectPluginSchema = createSelectSchema(plugins);
|
||||
|
||||
export const insertBackupSchema = createInsertSchema(backups);
|
||||
export const selectBackupSchema = createSelectSchema(backups);
|
||||
|
||||
export const insertScheduledTaskSchema = createInsertSchema(scheduledTasks);
|
||||
export const selectScheduledTaskSchema = createSelectSchema(scheduledTasks);
|
||||
|
||||
export const insertAuditLogSchema = createInsertSchema(auditLogs);
|
||||
export const selectAuditLogSchema = createSelectSchema(auditLogs);
|
||||
|
||||
export const insertServerSettingsSchema = createInsertSchema(serverSettings);
|
||||
export const selectServerSettingsSchema = createSelectSchema(serverSettings);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Inferred TypeScript types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type User = typeof users.$inferSelect;
|
||||
export type NewUser = typeof users.$inferInsert;
|
||||
|
||||
export type Session = typeof sessions.$inferSelect;
|
||||
export type NewSession = typeof sessions.$inferInsert;
|
||||
|
||||
export type Account = typeof accounts.$inferSelect;
|
||||
export type NewAccount = typeof accounts.$inferInsert;
|
||||
|
||||
export type Verification = typeof verifications.$inferSelect;
|
||||
export type NewVerification = typeof verifications.$inferInsert;
|
||||
|
||||
export type Invitation = typeof invitations.$inferSelect;
|
||||
export type NewInvitation = typeof invitations.$inferInsert;
|
||||
|
||||
export type McPlayer = typeof mcPlayers.$inferSelect;
|
||||
export type NewMcPlayer = typeof mcPlayers.$inferInsert;
|
||||
|
||||
export type PlayerBan = typeof playerBans.$inferSelect;
|
||||
export type NewPlayerBan = typeof playerBans.$inferInsert;
|
||||
|
||||
export type PlayerChatHistory = typeof playerChatHistory.$inferSelect;
|
||||
export type NewPlayerChatHistory = typeof playerChatHistory.$inferInsert;
|
||||
|
||||
export type PlayerSpawnPoint = typeof playerSpawnPoints.$inferSelect;
|
||||
export type NewPlayerSpawnPoint = typeof playerSpawnPoints.$inferInsert;
|
||||
|
||||
export type Plugin = typeof plugins.$inferSelect;
|
||||
export type NewPlugin = typeof plugins.$inferInsert;
|
||||
|
||||
export type Backup = typeof backups.$inferSelect;
|
||||
export type NewBackup = typeof backups.$inferInsert;
|
||||
|
||||
export type ScheduledTask = typeof scheduledTasks.$inferSelect;
|
||||
export type NewScheduledTask = typeof scheduledTasks.$inferInsert;
|
||||
|
||||
export type AuditLog = typeof auditLogs.$inferSelect;
|
||||
export type NewAuditLog = typeof auditLogs.$inferInsert;
|
||||
|
||||
export type ServerSettings = typeof serverSettings.$inferSelect;
|
||||
export type NewServerSettings = typeof serverSettings.$inferInsert;
|
||||
Reference in New Issue
Block a user