Add initial implementation of EMLy Bug Report API with MySQL integration and Docker support

This commit is contained in:
Flavio Fois
2026-02-16 11:11:50 +01:00
parent 1001321fe7
commit 0cad94dadd
19 changed files with 1055 additions and 0 deletions

28
src/db/connection.ts Normal file
View File

@@ -0,0 +1,28 @@
import mysql from "mysql2/promise";
import { config } from "../config";
let pool: mysql.Pool | null = null;
export function getPool(): mysql.Pool {
if (!pool) {
pool = mysql.createPool({
host: config.mysql.host,
port: config.mysql.port,
user: config.mysql.user,
password: config.mysql.password,
database: config.mysql.database,
waitForConnections: true,
connectionLimit: 10,
maxIdle: 5,
idleTimeout: 60000,
});
}
return pool;
}
export async function closePool(): Promise<void> {
if (pool) {
await pool.end();
pool = null;
}
}

59
src/db/migrate.ts Normal file
View File

@@ -0,0 +1,59 @@
import { readFileSync } from "fs";
import { join } from "path";
import { randomUUID } from "crypto";
import { hash } from "@node-rs/argon2";
import { getPool } from "./connection";
import { Log } from "../logger";
export async function runMigrations(): Promise<void> {
const pool = getPool();
const schemaPath = join(import.meta.dir, "schema.sql");
const schema = readFileSync(schemaPath, "utf-8");
// Split on semicolons, filter empty statements
const statements = schema
.split(";")
.map((s) => s.trim())
.filter((s) => s.length > 0);
for (const statement of statements) {
await pool.execute(statement);
}
// Additive migrations for existing databases
const alterMigrations = [
`ALTER TABLE bug_reports ADD COLUMN IF NOT EXISTS hostname VARCHAR(255) NOT NULL DEFAULT '' AFTER hwid`,
`ALTER TABLE bug_reports ADD COLUMN IF NOT EXISTS os_user VARCHAR(255) NOT NULL DEFAULT '' AFTER hostname`,
`ALTER TABLE bug_reports ADD INDEX IF NOT EXISTS idx_hostname (hostname)`,
`ALTER TABLE bug_reports ADD INDEX IF NOT EXISTS idx_os_user (os_user)`,
`ALTER TABLE user ADD COLUMN IF NOT EXISTS enabled BOOLEAN NOT NULL DEFAULT TRUE AFTER role`,
];
for (const migration of alterMigrations) {
try {
await pool.execute(migration);
} catch {
// Column/index already exists — safe to ignore
}
}
// Seed default admin user if user table is empty
const [rows] = await pool.execute("SELECT COUNT(*) as count FROM `user`");
const userCount = (rows as Array<{ count: number }>)[0].count;
if (userCount === 0) {
const passwordHash = await hash("admin", {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1
});
const id = randomUUID();
await pool.execute(
"INSERT INTO `user` (`id`, `username`, `password_hash`, `role`) VALUES (?, ?, ?, ?)",
[id, "admin", passwordHash, "admin"]
);
Log("MIGRATE", "Default admin user created (username: admin, password: admin)");
}
Log("MIGRATE", "Database migrations completed");
}

55
src/db/schema.sql Normal file
View File

@@ -0,0 +1,55 @@
CREATE TABLE IF NOT EXISTS `bug_reports` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`description` TEXT NOT NULL,
`hwid` VARCHAR(255) NOT NULL DEFAULT '',
`hostname` VARCHAR(255) NOT NULL DEFAULT '',
`os_user` VARCHAR(255) NOT NULL DEFAULT '',
`submitter_ip` VARCHAR(45) NOT NULL DEFAULT '',
`system_info` JSON NULL,
`status` ENUM('new', 'in_review', 'resolved', 'closed') NOT NULL DEFAULT 'new',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_status` (`status`),
INDEX `idx_hwid` (`hwid`),
INDEX `idx_hostname` (`hostname`),
INDEX `idx_os_user` (`os_user`),
INDEX `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `bug_report_files` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`report_id` INT UNSIGNED NOT NULL,
`file_role` ENUM('screenshot', 'mail_file', 'localstorage', 'config', 'system_info') NOT NULL,
`filename` VARCHAR(255) NOT NULL,
`mime_type` VARCHAR(127) NOT NULL DEFAULT 'application/octet-stream',
`file_size` INT UNSIGNED NOT NULL DEFAULT 0,
`data` LONGBLOB NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT `fk_report` FOREIGN KEY (`report_id`) REFERENCES `bug_reports`(`id`) ON DELETE CASCADE,
INDEX `idx_report_id` (`report_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `rate_limit_hwid` (
`hwid` VARCHAR(255) PRIMARY KEY,
`window_start` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`count` INT UNSIGNED NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `user` (
`id` VARCHAR(255) PRIMARY KEY,
`username` VARCHAR(255) NOT NULL UNIQUE,
`password_hash` VARCHAR(255) NOT NULL,
`role` ENUM('admin', 'user') NOT NULL DEFAULT 'user',
`enabled` BOOLEAN NOT NULL DEFAULT TRUE,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`displayname` VARCHAR(255) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `session` (
`id` VARCHAR(255) PRIMARY KEY,
`user_id` VARCHAR(255) NOT NULL,
`expires_at` DATETIME NOT NULL,
CONSTRAINT `fk_session_user` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;