Merge Bug Report and Update System into main #1
@@ -81,6 +81,7 @@ EMLy/
|
||||
├── app_viewer.go # Viewer window management (image, PDF, EML)
|
||||
├── app_screenshot.go # Screenshot capture functionality
|
||||
├── app_bugreport.go # Bug report creation and submission
|
||||
├── app_heartbeat.go # Bug report API heartbeat check
|
||||
├── app_settings.go # Settings import/export
|
||||
├── app_system.go # Windows system utilities (registry, encoding)
|
||||
├── main.go # Application entry point
|
||||
@@ -199,6 +200,7 @@ The Go backend is split into logical files:
|
||||
| `app_viewer.go` | Viewer windows: `OpenImageWindow`, `OpenPDFWindow`, `OpenEMLWindow`, `OpenPDF`, `OpenImage`, `GetViewerData` |
|
||||
| `app_screenshot.go` | Screenshots: `TakeScreenshot`, `SaveScreenshot`, `SaveScreenshotAs` |
|
||||
| `app_bugreport.go` | Bug reports: `CreateBugReportFolder`, `SubmitBugReport`, `zipFolder` |
|
||||
| `app_heartbeat.go` | API heartbeat: `CheckBugReportAPI` |
|
||||
| `app_settings.go` | Settings I/O: `ExportSettings`, `ImportSettings` |
|
||||
| `app_system.go` | System utilities: `CheckIsDefaultEMLHandler`, `OpenDefaultAppsSettings`, `ConvertToUTF8`, `OpenFolderInExplorer` |
|
||||
| `app_update.go` | Update system: `CheckForUpdates`, `DownloadUpdate`, `InstallUpdate`, `GetUpdateStatus` |
|
||||
@@ -254,6 +256,7 @@ The Go backend is split into logical files:
|
||||
| `CreateBugReportFolder()` | Creates folder with screenshot and mail file |
|
||||
| `SubmitBugReport(input)` | Creates complete bug report with ZIP archive, attempts server upload |
|
||||
| `UploadBugReport(folderPath, input)` | Uploads bug report files to configured API server via multipart POST |
|
||||
| `CheckBugReportAPI()` | Checks if the bug report API is reachable via /health endpoint (3s timeout) |
|
||||
|
||||
**Settings (`app_settings.go`)**
|
||||
|
||||
@@ -673,9 +676,14 @@ Complete bug reporting system:
|
||||
3. Includes current mail file if loaded
|
||||
4. Gathers system information
|
||||
5. Creates ZIP archive in temp folder
|
||||
6. Attempts to upload to the bug report API server (if configured)
|
||||
7. Falls back to local ZIP if server is unreachable
|
||||
8. Shows server confirmation with report ID, or local path with upload warning
|
||||
6. Checks if the bug report API is online via heartbeat (`CheckBugReportAPI`)
|
||||
7. If online, attempts to upload to the bug report API server
|
||||
8. Falls back to local ZIP if server is offline or upload fails
|
||||
9. Shows server confirmation with report ID, or local path with upload warning
|
||||
|
||||
#### Heartbeat Check (`app_heartbeat.go`)
|
||||
|
||||
Before uploading a bug report, the app sends a GET request to `{BUGREPORT_API_URL}/health` with a 3-second timeout. If the API doesn't respond with status 200, the upload is skipped entirely and only the local ZIP is created. The `CheckBugReportAPI()` method is also exposed to the frontend for UI status checks.
|
||||
|
||||
#### Bug Report API Server
|
||||
|
||||
@@ -684,6 +692,7 @@ A separate API server (`server/` directory) receives bug reports:
|
||||
- **Deployment**: Docker Compose (`docker compose up -d` from `server/`)
|
||||
- **Auth**: Static API key for clients (`X-API-Key`), static admin key (`X-Admin-Key`)
|
||||
- **Rate limiting**: HWID-based, configurable (default 5 reports per 24h)
|
||||
- **Logging**: Structured file logging to `logs/api.log` with format `[date] - [time] - [source] - message`
|
||||
- **Endpoints**: `POST /api/bug-reports` (client), `GET/DELETE /api/admin/bug-reports` (admin)
|
||||
|
||||
#### Bug Report Dashboard
|
||||
|
||||
@@ -249,14 +249,19 @@ External IP: %s
|
||||
FolderPath: bugReportFolder,
|
||||
}
|
||||
|
||||
// Attempt to upload to the bug report API server
|
||||
reportID, uploadErr := a.UploadBugReport(bugReportFolder, input)
|
||||
if uploadErr != nil {
|
||||
Log("Bug report upload failed (falling back to local zip):", uploadErr)
|
||||
result.UploadError = uploadErr.Error()
|
||||
// Attempt to upload to the bug report API server (only if reachable)
|
||||
if !a.CheckBugReportAPI() {
|
||||
Log("Bug report API is offline, skipping upload")
|
||||
result.UploadError = "Bug report API is offline"
|
||||
} else {
|
||||
result.Uploaded = true
|
||||
result.ReportID = reportID
|
||||
reportID, uploadErr := a.UploadBugReport(bugReportFolder, input)
|
||||
if uploadErr != nil {
|
||||
Log("Bug report upload failed (falling back to local zip):", uploadErr)
|
||||
result.UploadError = uploadErr.Error()
|
||||
} else {
|
||||
result.Uploaded = true
|
||||
result.ReportID = reportID
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
45
app_heartbeat.go
Normal file
45
app_heartbeat.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Package main provides heartbeat checking for the bug report API.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"emly/backend/utils"
|
||||
)
|
||||
|
||||
// CheckBugReportAPI sends a GET request to the bug report API's /health
|
||||
// endpoint with a short timeout. Returns true if the API responds with
|
||||
// status 200, false otherwise. This is exposed to the frontend.
|
||||
func (a *App) CheckBugReportAPI() bool {
|
||||
cfgPath := utils.DefaultConfigPath()
|
||||
cfg, err := utils.LoadConfig(cfgPath)
|
||||
if err != nil {
|
||||
Log("Heartbeat: failed to load config:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
apiURL := cfg.EMLy.BugReportAPIURL
|
||||
if apiURL == "" {
|
||||
Log("Heartbeat: bug report API URL not configured")
|
||||
return false
|
||||
}
|
||||
|
||||
endpoint := apiURL + "/health"
|
||||
client := &http.Client{Timeout: 3 * time.Second}
|
||||
|
||||
resp, err := client.Get(endpoint)
|
||||
if err != nil {
|
||||
Log("Heartbeat: API unreachable:", err)
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
Log(fmt.Sprintf("Heartbeat: API returned status %d", resp.StatusCode))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -7,5 +7,5 @@ LANGUAGE = it
|
||||
UPDATE_CHECK_ENABLED = false
|
||||
UPDATE_PATH =
|
||||
UPDATE_AUTO_CHECK = false
|
||||
BUGREPORT_API_URL = "https://api.whiskr.it"
|
||||
BUGREPORT_API_URL = "https://emly-api.lyzcoote.cloud"
|
||||
BUGREPORT_API_KEY = "emly_1BaQdBknsMGcY5DynSby71JnWOKXtJvnuUprkgWT0pujpLFxj5HaTXP9vtJAMk63"
|
||||
85
server/compose-dev.yml
Normal file
85
server/compose-dev.yml
Normal file
@@ -0,0 +1,85 @@
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:lts
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-emly_bugreports}
|
||||
MYSQL_USER: ${MYSQL_USER:-emly}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
emly:
|
||||
ipv4_address: 172.16.32.2
|
||||
|
||||
api:
|
||||
build: .
|
||||
environment:
|
||||
MYSQL_HOST: mysql
|
||||
MYSQL_PORT: 3306
|
||||
MYSQL_USER: ${MYSQL_USER:-emly}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-emly_bugreports}
|
||||
API_KEY: ${API_KEY}
|
||||
ADMIN_KEY: ${ADMIN_KEY}
|
||||
PORT: 3000
|
||||
RATE_LIMIT_MAX: ${RATE_LIMIT_MAX:-5}
|
||||
RATE_LIMIT_WINDOW_HOURS: ${RATE_LIMIT_WINDOW_HOURS:-24}
|
||||
volumes:
|
||||
- ./logs/api:/app/logs
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
emly:
|
||||
ipv4_address: 172.16.32.3
|
||||
|
||||
dashboard:
|
||||
build: ./dashboard
|
||||
environment:
|
||||
MYSQL_HOST: mysql
|
||||
MYSQL_PORT: 3306
|
||||
MYSQL_USER: ${MYSQL_USER:-emly}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-emly_bugreports}
|
||||
volumes:
|
||||
- ./logs/dashboard:/app/logs
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
emly:
|
||||
ipv4_address: 172.16.32.4
|
||||
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
command: tunnel run
|
||||
environment:
|
||||
TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN_DEV}
|
||||
depends_on:
|
||||
- api
|
||||
- dashboard
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
emly:
|
||||
ipv4_address: 172.16.32.5
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
|
||||
networks:
|
||||
emly:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.16.32.0/24
|
||||
gateway: 172.16.32.1
|
||||
@@ -33,6 +33,8 @@ services:
|
||||
PORT: 3000
|
||||
RATE_LIMIT_MAX: ${RATE_LIMIT_MAX:-5}
|
||||
RATE_LIMIT_WINDOW_HOURS: ${RATE_LIMIT_WINDOW_HOURS:-24}
|
||||
volumes:
|
||||
- ./logs/api:/app/logs
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
mysql:
|
||||
@@ -49,10 +51,11 @@ services:
|
||||
MYSQL_USER: ${MYSQL_USER:-emly}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-emly_bugreports}
|
||||
volumes:
|
||||
- ./logs/dashboard:/app/logs
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
|
||||
networks:
|
||||
emly:
|
||||
ipv4_address: 172.16.32.4
|
||||
@@ -1,7 +1,18 @@
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import { lucia } from '$lib/server/auth';
|
||||
import { initLogger, Log } from '$lib/server/logger';
|
||||
|
||||
// Initialize dashboard logger
|
||||
initLogger();
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
const ip =
|
||||
event.request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
|
||||
event.request.headers.get('x-real-ip') ||
|
||||
event.getClientAddress?.() ||
|
||||
'unknown';
|
||||
Log('HTTP', `${event.request.method} ${event.url.pathname} from ${ip}`);
|
||||
|
||||
const sessionId = event.cookies.get(lucia.sessionCookieName);
|
||||
|
||||
if (!sessionId) {
|
||||
@@ -21,6 +32,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
Log('AUTH', `Invalid session from ip=${ip}`);
|
||||
const sessionCookie = lucia.createBlankSessionCookie();
|
||||
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
||||
path: '.',
|
||||
@@ -30,6 +42,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
|
||||
// If user is disabled, invalidate their session and clear cookie
|
||||
if (session && user && !user.enabled) {
|
||||
Log('AUTH', `Disabled user rejected: username=${user.username} ip=${ip}`);
|
||||
await lucia.invalidateSession(session.id);
|
||||
const sessionCookie = lucia.createBlankSessionCookie();
|
||||
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
||||
|
||||
42
server/dashboard/src/lib/server/logger.ts
Normal file
42
server/dashboard/src/lib/server/logger.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { mkdirSync, appendFileSync, existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
let logFilePath: string | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the logger. Creates the logs/ directory if needed
|
||||
* and opens the log file in append mode.
|
||||
*/
|
||||
export function initLogger(filename = "dashboard.log"): void {
|
||||
const logsDir = join(process.cwd(), "logs");
|
||||
if (!existsSync(logsDir)) {
|
||||
mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
logFilePath = join(logsDir, filename);
|
||||
Log("LOGGER", "Logger initialized. Writing to:", logFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a timestamped, source-tagged message to stdout and the log file.
|
||||
* Format: [YYYY-MM-DD] - [HH:MM:SS] - [source] - message
|
||||
*/
|
||||
export function Log(source: string, ...args: unknown[]): void {
|
||||
const now = new Date();
|
||||
const date = now.toISOString().slice(0, 10);
|
||||
const time = now.toTimeString().slice(0, 8);
|
||||
const msg = args
|
||||
.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a)))
|
||||
.join(" ");
|
||||
|
||||
const line = `[${date}] - [${time}] - [${source}] - ${msg}`;
|
||||
|
||||
console.log(line);
|
||||
|
||||
if (logFilePath) {
|
||||
try {
|
||||
appendFileSync(logFilePath, line + "\n");
|
||||
} catch {
|
||||
// If file write fails, stdout logging still works
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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();
|
||||
@@ -51,8 +52,8 @@ export async function runMigrations(): Promise<void> {
|
||||
"INSERT INTO `user` (`id`, `username`, `password_hash`, `role`) VALUES (?, ?, ?, ?)",
|
||||
[id, "admin", passwordHash, "admin"]
|
||||
);
|
||||
console.log("Default admin user created (username: admin, password: admin)");
|
||||
Log("MIGRATE", "Default admin user created (username: admin, password: admin)");
|
||||
}
|
||||
|
||||
console.log("Database migrations completed");
|
||||
Log("MIGRATE", "Database migrations completed");
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import { runMigrations } from "./db/migrate";
|
||||
import { closePool } from "./db/connection";
|
||||
import { bugReportRoutes } from "./routes/bugReports";
|
||||
import { adminRoutes } from "./routes/admin";
|
||||
import { initLogger, Log } from "./logger";
|
||||
|
||||
// Initialize logger
|
||||
initLogger();
|
||||
|
||||
// Validate environment
|
||||
validateConfig();
|
||||
@@ -12,8 +16,20 @@ validateConfig();
|
||||
await runMigrations();
|
||||
|
||||
const app = new Elysia()
|
||||
.onRequest(({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const ip =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"unknown";
|
||||
Log("HTTP", `${request.method} ${url.pathname} from ${ip}`);
|
||||
})
|
||||
.onAfterResponse(({ request, set }) => {
|
||||
const url = new URL(request.url);
|
||||
Log("HTTP", `${request.method} ${url.pathname} -> ${set.status ?? 200}`);
|
||||
})
|
||||
.onError(({ error, set }) => {
|
||||
console.error("Unhandled error:", error);
|
||||
Log("ERROR", "Unhandled error:", error);
|
||||
set.status = 500;
|
||||
return { success: false, message: "Internal server error" };
|
||||
})
|
||||
@@ -25,19 +41,20 @@ const app = new Elysia()
|
||||
maxBody: 50 * 1024 * 1024, // 50MB
|
||||
});
|
||||
|
||||
console.log(
|
||||
Log(
|
||||
"SERVER",
|
||||
`EMLy Bug Report API running on http://localhost:${app.server?.port}`
|
||||
);
|
||||
|
||||
// Graceful shutdown
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("Shutting down...");
|
||||
Log("SERVER", "Shutting down (SIGINT)...");
|
||||
await closePool();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
console.log("Shutting down...");
|
||||
Log("SERVER", "Shutting down (SIGTERM)...");
|
||||
await closePool();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
42
server/src/logger.ts
Normal file
42
server/src/logger.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { mkdirSync, appendFileSync, existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
let logFilePath: string | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the logger. Creates the logs/ directory if needed
|
||||
* and opens the log file in append mode.
|
||||
*/
|
||||
export function initLogger(filename = "api.log"): void {
|
||||
const logsDir = join(process.cwd(), "logs");
|
||||
if (!existsSync(logsDir)) {
|
||||
mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
logFilePath = join(logsDir, filename);
|
||||
Log("LOGGER", "Logger initialized. Writing to:", logFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a timestamped, source-tagged message to stdout and the log file.
|
||||
* Format: [YYYY-MM-DD] - [HH:MM:SS] - [source] - message
|
||||
*/
|
||||
export function Log(source: string, ...args: unknown[]): void {
|
||||
const now = new Date();
|
||||
const date = now.toISOString().slice(0, 10);
|
||||
const time = now.toTimeString().slice(0, 8);
|
||||
const msg = args
|
||||
.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a)))
|
||||
.join(" ");
|
||||
|
||||
const line = `[${date}] - [${time}] - [${source}] - ${msg}`;
|
||||
|
||||
console.log(line);
|
||||
|
||||
if (logFilePath) {
|
||||
try {
|
||||
appendFileSync(logFilePath, line + "\n");
|
||||
} catch {
|
||||
// If file write fails, stdout logging still works
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Elysia } from "elysia";
|
||||
import { config } from "../config";
|
||||
import { Log } from "../logger";
|
||||
|
||||
export const apiKeyGuard = new Elysia({ name: "api-key-guard" }).derive(
|
||||
{ as: "scoped" },
|
||||
({ headers, error }) => {
|
||||
({ headers, error, request }) => {
|
||||
const key = headers["x-api-key"];
|
||||
if (!key || key !== config.apiKey) {
|
||||
const ip =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"unknown";
|
||||
Log("AUTH", `Invalid API key from ip=${ip}`);
|
||||
return error(401, { success: false, message: "Invalid or missing API key" });
|
||||
}
|
||||
return {};
|
||||
@@ -14,9 +20,14 @@ export const apiKeyGuard = new Elysia({ name: "api-key-guard" }).derive(
|
||||
|
||||
export const adminKeyGuard = new Elysia({ name: "admin-key-guard" }).derive(
|
||||
{ as: "scoped" },
|
||||
({ headers, error }) => {
|
||||
({ headers, error, request }) => {
|
||||
const key = headers["x-admin-key"];
|
||||
if (!key || key !== config.adminKey) {
|
||||
const ip =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"unknown";
|
||||
Log("AUTH", `Invalid admin key from ip=${ip}`);
|
||||
return error(401, { success: false, message: "Invalid or missing admin key" });
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Elysia } from "elysia";
|
||||
import { getPool } from "../db/connection";
|
||||
import { config } from "../config";
|
||||
import { Log } from "../logger";
|
||||
|
||||
const excludedHwids = new Set<string>([
|
||||
// Add HWIDs here for development testing
|
||||
@@ -54,6 +55,7 @@ export const hwidRateLimit = new Elysia({ name: "hwid-rate-limit" }).derive(
|
||||
if (entry.count >= config.rateLimit.max) {
|
||||
const retryAfterMs = windowMs - elapsed;
|
||||
const retryAfterMin = Math.ceil(retryAfterMs / 60000);
|
||||
Log("RATELIMIT", `Rate limit hit hwid=${hwid} count=${entry.count}`);
|
||||
return error(429, {
|
||||
success: false,
|
||||
message: `Rate limit exceeded. Try again in ${retryAfterMin} minutes.`,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
deleteBugReport,
|
||||
updateBugReportStatus,
|
||||
} from "../services/bugReportService";
|
||||
import { Log } from "../logger";
|
||||
import type { BugReportStatus } from "../types";
|
||||
|
||||
export const adminRoutes = new Elysia({ prefix: "/api/admin" })
|
||||
@@ -18,6 +19,7 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
|
||||
const pageSize = Math.min(parseInt(query.pageSize || "20"), 100);
|
||||
const status = query.status as BugReportStatus | undefined;
|
||||
|
||||
Log("ADMIN", `List bug reports page=${page} pageSize=${pageSize} status=${status || "all"}`);
|
||||
return await listBugReports({ page, pageSize, status });
|
||||
},
|
||||
{
|
||||
@@ -39,6 +41,7 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
|
||||
.get(
|
||||
"/bug-reports/:id",
|
||||
async ({ params, error }) => {
|
||||
Log("ADMIN", `Get bug report id=${params.id}`);
|
||||
const result = await getBugReport(parseInt(params.id));
|
||||
if (!result) return error(404, { success: false, message: "Report not found" });
|
||||
return result;
|
||||
@@ -51,6 +54,7 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
|
||||
.patch(
|
||||
"/bug-reports/:id/status",
|
||||
async ({ params, body, error }) => {
|
||||
Log("ADMIN", `Update status id=${params.id} status=${body.status}`);
|
||||
const updated = await updateBugReportStatus(
|
||||
parseInt(params.id),
|
||||
body.status
|
||||
@@ -92,6 +96,7 @@ export const adminRoutes = new Elysia({ prefix: "/api/admin" })
|
||||
.delete(
|
||||
"/bug-reports/:id",
|
||||
async ({ params, error }) => {
|
||||
Log("ADMIN", `Delete bug report id=${params.id}`);
|
||||
const deleted = await deleteBugReport(parseInt(params.id));
|
||||
if (!deleted)
|
||||
return error(404, { success: false, message: "Report not found" });
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Elysia, t } from "elysia";
|
||||
import { apiKeyGuard } from "../middleware/auth";
|
||||
import { hwidRateLimit } from "../middleware/rateLimit";
|
||||
import { createBugReport, addFile } from "../services/bugReportService";
|
||||
import { Log } from "../logger";
|
||||
import type { FileRole } from "../types";
|
||||
|
||||
const FILE_ROLES: { field: string; role: FileRole; mime: string }[] = [
|
||||
@@ -19,6 +20,14 @@ export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" })
|
||||
async ({ body, request, set }) => {
|
||||
const { name, email, description, hwid, hostname, os_user, system_info } = body;
|
||||
|
||||
// Get submitter IP from headers or connection
|
||||
const submitterIp =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"unknown";
|
||||
|
||||
Log("BUGREPORT", `Received from name=${name} hwid=${hwid || "none"} ip=${submitterIp}`);
|
||||
|
||||
// Parse system_info — may arrive as a JSON string or already-parsed object
|
||||
let systemInfo: Record<string, unknown> | null = null;
|
||||
if (system_info) {
|
||||
@@ -33,12 +42,6 @@ export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" })
|
||||
}
|
||||
}
|
||||
|
||||
// Get submitter IP from headers or connection
|
||||
const submitterIp =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"unknown";
|
||||
|
||||
// Create the bug report
|
||||
const reportId = await createBugReport({
|
||||
name,
|
||||
@@ -56,6 +59,7 @@ export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" })
|
||||
const file = body[field as keyof typeof body];
|
||||
if (file && file instanceof File) {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
Log("BUGREPORT", `File uploaded: role=${role} size=${buffer.length} bytes`);
|
||||
await addFile({
|
||||
report_id: reportId,
|
||||
file_role: role,
|
||||
@@ -67,6 +71,8 @@ export const bugReportRoutes = new Elysia({ prefix: "/api/bug-reports" })
|
||||
}
|
||||
}
|
||||
|
||||
Log("BUGREPORT", `Created successfully with id=${reportId}`);
|
||||
|
||||
set.status = 201;
|
||||
return {
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user