coldown fix
This commit is contained in:
@@ -11,6 +11,7 @@ interface Config {
|
||||
|
||||
autoExportIntervalSeconds: number;
|
||||
exportPath: string;
|
||||
adminKey: string;
|
||||
|
||||
enableKeycloak: boolean;
|
||||
keycloakRealm: string;
|
||||
@@ -22,87 +23,44 @@ function parseConfigFile(): Config {
|
||||
try {
|
||||
const configPath = join(process.cwd(), "config.cfg");
|
||||
const configContent = readFileSync(configPath, "utf-8");
|
||||
|
||||
const config: Partial<Config> = {};
|
||||
|
||||
|
||||
const configMap = new Map<string, string>();
|
||||
configContent.split("\n").forEach(line => {
|
||||
line = line.trim();
|
||||
if (line.startsWith("#") || !line.includes("=")) return;
|
||||
|
||||
const [key, value] = line.split("=");
|
||||
const trimmedKey = key.trim();
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
switch (trimmedKey) {
|
||||
case "CANVAS_WIDTH":
|
||||
config.canvasWidth = parseInt(trimmedValue);
|
||||
break;
|
||||
case "CANVAS_HEIGHT":
|
||||
config.canvasHeight = parseInt(trimmedValue);
|
||||
break;
|
||||
case "DEFAULT_COOLDOWN":
|
||||
config.defaultCooldown = parseInt(trimmedValue);
|
||||
break;
|
||||
case "ENABLE_AUTOMATIC_EVENTS":
|
||||
config.enableAutomaticEvents = trimmedValue.toLowerCase() === "true";
|
||||
break;
|
||||
case "EVENT_DURATION_MINUTES":
|
||||
config.eventDurationMinutes = parseInt(trimmedValue);
|
||||
break;
|
||||
case "EVENT_INTERVAL_HOURS":
|
||||
config.eventIntervalHours = parseInt(trimmedValue);
|
||||
break;
|
||||
|
||||
case "AUTO_EXPORT_INTERVAL_SECONDS":
|
||||
config.autoExportIntervalSeconds = parseInt(trimmedValue);
|
||||
break;
|
||||
case "EXPORT_PATH":
|
||||
config.exportPath = trimmedValue;
|
||||
break;
|
||||
case "ENABLE_KEYCLOAK":
|
||||
config.enableKeycloak = trimmedValue.toLowerCase() === "true";
|
||||
break;
|
||||
case "KEYCLOAK_REALM":
|
||||
config.keycloakRealm = trimmedValue;
|
||||
break;
|
||||
case "KEYCLOAK_AUTH_URL":
|
||||
config.keycloakAuthUrl = trimmedValue;
|
||||
break;
|
||||
case "KEYCLOAK_CLIENT_ID":
|
||||
config.keycloakClientId = trimmedValue;
|
||||
break;
|
||||
}
|
||||
configMap.set(key.trim(), value.trim());
|
||||
});
|
||||
|
||||
// Set defaults for missing values
|
||||
|
||||
|
||||
return {
|
||||
canvasWidth: config.canvasWidth || 100,
|
||||
canvasHeight: config.canvasHeight || 100,
|
||||
defaultCooldown: config.defaultCooldown || 5,
|
||||
enableAutomaticEvents: config.enableAutomaticEvents || false,
|
||||
eventDurationMinutes: config.eventDurationMinutes || 30,
|
||||
eventIntervalHours: config.eventIntervalHours || 6,
|
||||
|
||||
autoExportIntervalSeconds: config.autoExportIntervalSeconds || 60,
|
||||
exportPath: config.exportPath || "./exports/",
|
||||
|
||||
enableKeycloak: config.enableKeycloak || false,
|
||||
keycloakRealm: config.keycloakRealm || "rplace",
|
||||
keycloakAuthUrl: config.keycloakAuthUrl || "http://localhost:8080/auth",
|
||||
keycloakClientId: config.keycloakClientId || "rplace-client",
|
||||
canvasWidth: parseInt(configMap.get('CANVAS_WIDTH') || '500'),
|
||||
canvasHeight: parseInt(configMap.get('CANVAS_HEIGHT') || '200'),
|
||||
defaultCooldown: parseInt(configMap.get('DEFAULT_COOLDOWN') || '10'),
|
||||
enableAutomaticEvents: configMap.get('ENABLE_AUTOMATIC_EVENTS') === 'true',
|
||||
eventDurationMinutes: parseInt(configMap.get('EVENT_DURATION_MINUTES') || '30'),
|
||||
eventIntervalHours: parseInt(configMap.get('EVENT_INTERVAL_HOURS') || '1'),
|
||||
autoExportIntervalSeconds: parseInt(configMap.get('AUTO_EXPORT_INTERVAL_SECONDS') || '60'),
|
||||
exportPath: configMap.get('EXPORT_PATH') || './exports/',
|
||||
adminKey: configMap.get('ADMIN_KEY') || 'admin123',
|
||||
enableKeycloak: configMap.get('ENABLE_KEYCLOAK') === 'true',
|
||||
keycloakRealm: configMap.get('KEYCLOAK_REALM') || 'rplace',
|
||||
keycloakAuthUrl: configMap.get('KEYCLOAK_AUTH_URL') || 'http://localhost:8080/auth',
|
||||
keycloakClientId: configMap.get('KEYCLOAK_CLIENT_ID') || 'rplace-client',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error reading config file, using defaults:", error);
|
||||
return {
|
||||
canvasWidth: 100,
|
||||
canvasHeight: 100,
|
||||
defaultCooldown: 5,
|
||||
canvasWidth: 500,
|
||||
canvasHeight: 200,
|
||||
defaultCooldown: 10,
|
||||
enableAutomaticEvents: false,
|
||||
eventDurationMinutes: 30,
|
||||
eventIntervalHours: 6,
|
||||
eventIntervalHours: 1,
|
||||
|
||||
autoExportIntervalSeconds: 60,
|
||||
exportPath: "./exports/",
|
||||
adminKey: 'admin123',
|
||||
|
||||
enableKeycloak: false,
|
||||
keycloakRealm: "rplace",
|
||||
|
||||
@@ -3,6 +3,8 @@ import { registerRoutes } from "./routes";
|
||||
import { setupVite, serveStatic, log } from "./vite";
|
||||
import { setupKeycloak } from "./keycloak";
|
||||
import { config } from "./config";
|
||||
import { storage } from "./storage";
|
||||
import { CanvasExporter } from "./export";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -15,7 +17,7 @@ if (config.enableKeycloak) {
|
||||
process.env.KEYCLOAK_REALM = config.keycloakRealm;
|
||||
process.env.KEYCLOAK_AUTH_URL = config.keycloakAuthUrl;
|
||||
process.env.KEYCLOAK_CLIENT_ID = config.keycloakClientId;
|
||||
|
||||
|
||||
keycloak = setupKeycloak(app);
|
||||
log("Keycloak authentication enabled");
|
||||
} else {
|
||||
@@ -72,6 +74,42 @@ app.use((req, res, next) => {
|
||||
serveStatic(app);
|
||||
}
|
||||
|
||||
// Aktualisiere Canvas-Konfiguration beim Start falls sich config.cfg geändert hat
|
||||
try {
|
||||
const currentConfig = await storage.getCanvasConfig();
|
||||
const configChanged =
|
||||
currentConfig.canvasWidth !== config.canvasWidth ||
|
||||
currentConfig.canvasHeight !== config.canvasHeight ||
|
||||
currentConfig.defaultCooldown !== config.defaultCooldown ||
|
||||
currentConfig.enableAutomaticEvents !== config.enableAutomaticEvents ||
|
||||
currentConfig.eventDuration !== config.eventDurationMinutes ||
|
||||
currentConfig.eventInterval !== config.eventIntervalHours;
|
||||
|
||||
if (configChanged) {
|
||||
console.log(`${formatTime()} [express] Aktualisiere Canvas-Konfiguration aus config.cfg`);
|
||||
|
||||
// Für SQLite Storage: Verwende expandCanvas wenn Canvas vergrößert wird
|
||||
if ('expandCanvas' in storage && typeof storage.expandCanvas === 'function' &&
|
||||
(config.canvasWidth > currentConfig.canvasWidth || config.canvasHeight > currentConfig.canvasHeight)) {
|
||||
await (storage as any).expandCanvas(config.canvasWidth, config.canvasHeight);
|
||||
} else {
|
||||
await storage.updateCanvasConfig({
|
||||
canvasWidth: Math.max(currentConfig.canvasWidth, config.canvasWidth), // Erlaube nur Erweiterung
|
||||
canvasHeight: Math.max(currentConfig.canvasHeight, config.canvasHeight), // Erlaube nur Erweiterung
|
||||
defaultCooldown: config.defaultCooldown,
|
||||
enableAutomaticEvents: config.enableAutomaticEvents,
|
||||
eventDuration: config.eventDurationMinutes,
|
||||
eventInterval: config.eventIntervalHours
|
||||
});
|
||||
}
|
||||
console.log(`${formatTime()} [express] Canvas-Konfiguration aktualisiert`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${formatTime()} [express] Fehler beim Aktualisieren der Canvas-Konfiguration:`, error);
|
||||
}
|
||||
|
||||
// Canvas exporter wird bereits in routes.ts initialisiert
|
||||
|
||||
// ALWAYS serve the app on the port specified in the environment variable PORT
|
||||
// Other ports are firewalled. Default to 5000 if not specified.
|
||||
// this serves both the API and the client.
|
||||
@@ -85,3 +123,11 @@ app.use((req, res, next) => {
|
||||
log(`serving on port ${port}`);
|
||||
});
|
||||
})();
|
||||
|
||||
function formatTime() {
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
return `[${hours}:${minutes}:${seconds}]`;
|
||||
}
|
||||
142
server/routes.ts
142
server/routes.ts
@@ -6,6 +6,17 @@ import { insertPixelSchema, insertUserCooldownSchema, type WSMessage } from "@sh
|
||||
import { CanvasExporter } from "./export";
|
||||
import { config } from "./config";
|
||||
|
||||
// Admin authentication middleware
|
||||
function requireAdmin(req: Request, res: Response, next: NextFunction) {
|
||||
const adminKey = req.headers['x-admin-key'] || req.body.adminKey;
|
||||
|
||||
if (!adminKey || adminKey !== config.adminKey) {
|
||||
return res.status(401).json({ message: "Admin access required" });
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
// Authentication middleware
|
||||
function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
if (!config.enableKeycloak) {
|
||||
@@ -99,6 +110,48 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Canvas-Erweiterungs-Endpoint
|
||||
app.post("/api/config/expand", async (req, res) => {
|
||||
try {
|
||||
const { canvasWidth, canvasHeight } = req.body;
|
||||
|
||||
if (!canvasWidth || !canvasHeight || canvasWidth < 1 || canvasHeight < 1) {
|
||||
return res.status(400).json({ message: "Invalid canvas dimensions" });
|
||||
}
|
||||
|
||||
const currentConfig = await storage.getCanvasConfig();
|
||||
|
||||
// Erlaube nur Erweiterung, nicht Verkleinerung
|
||||
if (canvasWidth < currentConfig.canvasWidth || canvasHeight < currentConfig.canvasHeight) {
|
||||
return res.status(400).json({
|
||||
message: "Canvas kann nur erweitert werden, nicht verkleinert",
|
||||
current: { width: currentConfig.canvasWidth, height: currentConfig.canvasHeight }
|
||||
});
|
||||
}
|
||||
|
||||
// Für SQLite Storage: Verwende spezielle expandCanvas Methode
|
||||
if ('expandCanvas' in storage && typeof storage.expandCanvas === 'function') {
|
||||
await (storage as any).expandCanvas(canvasWidth, canvasHeight);
|
||||
} else {
|
||||
// Fallback für Memory Storage
|
||||
await storage.updateCanvasConfig({
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
defaultCooldown: currentConfig.defaultCooldown,
|
||||
enableAutomaticEvents: currentConfig.enableAutomaticEvents,
|
||||
eventDuration: currentConfig.eventDuration,
|
||||
eventInterval: currentConfig.eventInterval
|
||||
});
|
||||
}
|
||||
|
||||
const updatedConfig = await storage.getCanvasConfig();
|
||||
res.json(updatedConfig);
|
||||
} catch (error) {
|
||||
console.error("Failed to expand canvas:", error);
|
||||
res.status(500).json({ message: "Failed to expand canvas" });
|
||||
}
|
||||
});
|
||||
|
||||
// Config is now read-only from file
|
||||
// Remove the POST endpoint for config updates
|
||||
|
||||
@@ -121,11 +174,17 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Check cooldown unless events are enabled
|
||||
if (!config.enableAutomaticEvents) {
|
||||
const cooldown = await storage.getUserCooldown(userInfo.userId);
|
||||
if (cooldown && cooldown.cooldownEnds > new Date()) {
|
||||
return res.status(429).json({ message: "Cooldown active" });
|
||||
const now = new Date();
|
||||
|
||||
if (cooldown && cooldown.cooldownEnds > now) {
|
||||
const remaining = Math.ceil((cooldown.cooldownEnds.getTime() - now.getTime()) / 1000);
|
||||
return res.status(429).json({
|
||||
message: "Cooldown active",
|
||||
remainingSeconds: remaining
|
||||
});
|
||||
}
|
||||
|
||||
// Set new cooldown
|
||||
// Set new cooldown - immer setzen, auch wenn kein vorheriger existierte
|
||||
const cooldownEnd = new Date(Date.now() + (config.defaultCooldown * 1000));
|
||||
await storage.setUserCooldown({
|
||||
userId: userInfo.userId,
|
||||
@@ -226,6 +285,83 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
});
|
||||
});
|
||||
|
||||
// Admin Routes
|
||||
app.post("/api/admin/auth", (req, res) => {
|
||||
const { adminKey } = req.body;
|
||||
|
||||
if (adminKey === config.adminKey) {
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(401).json({ message: "Invalid admin key" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/admin/stats", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const pixels = await storage.getAllPixels();
|
||||
const uniqueUsers = new Set(pixels.map(p => p.userId)).size;
|
||||
|
||||
res.json({
|
||||
totalPixels: pixels.length,
|
||||
uniqueUsers,
|
||||
lastActivity: pixels.length > 0 ? pixels[pixels.length - 1].createdAt : null
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch admin stats" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/admin/pixels/:id", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if ('deletePixel' in storage && typeof storage.deletePixel === 'function') {
|
||||
await (storage as any).deletePixel(id);
|
||||
|
||||
// Broadcast pixel deletion
|
||||
broadcast({
|
||||
type: "pixel_deleted",
|
||||
data: { pixelId: id },
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(501).json({ message: "Pixel deletion not supported by current storage" });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to delete pixel" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/admin/canvas", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
if ('clearCanvas' in storage && typeof storage.clearCanvas === 'function') {
|
||||
await (storage as any).clearCanvas();
|
||||
|
||||
// Broadcast canvas clear
|
||||
broadcast({
|
||||
type: "canvas_cleared",
|
||||
data: {},
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(501).json({ message: "Canvas clearing not supported by current storage" });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to clear canvas" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/admin/export", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const filename = await exporter.exportCanvas();
|
||||
res.json({ filename, success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to export canvas" });
|
||||
}
|
||||
});
|
||||
|
||||
// Keep connections alive with ping/pong
|
||||
const pingInterval = setInterval(() => {
|
||||
connectedUsers.forEach(ws => {
|
||||
|
||||
220
server/sqlite-storage.ts
Normal file
220
server/sqlite-storage.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { type Pixel, type InsertPixel, type CanvasConfig, type InsertCanvasConfig, type UserCooldown, type InsertUserCooldown } from "@shared/schema";
|
||||
import { randomUUID } from "crypto";
|
||||
import { config } from "./config";
|
||||
import { IStorage } from "./storage";
|
||||
|
||||
export class SQLiteStorage implements IStorage {
|
||||
private db: Database.Database;
|
||||
|
||||
constructor(dbPath: string = ':memory:') {
|
||||
this.db = new Database(dbPath);
|
||||
this.initTables();
|
||||
this.initDefaultConfig();
|
||||
}
|
||||
|
||||
private initTables() {
|
||||
// Pixels table
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS pixels (
|
||||
id TEXT PRIMARY KEY DEFAULT (hex(randomblob(16))),
|
||||
x INTEGER NOT NULL,
|
||||
y INTEGER NOT NULL,
|
||||
color TEXT NOT NULL,
|
||||
userId TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Canvas config table
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS canvas_config (
|
||||
id TEXT PRIMARY KEY DEFAULT (hex(randomblob(16))),
|
||||
canvasWidth INTEGER NOT NULL DEFAULT 100,
|
||||
canvasHeight INTEGER NOT NULL DEFAULT 100,
|
||||
defaultCooldown INTEGER NOT NULL DEFAULT 5,
|
||||
enableAutomaticEvents BOOLEAN NOT NULL DEFAULT 0,
|
||||
eventDuration INTEGER NOT NULL DEFAULT 30,
|
||||
eventInterval INTEGER NOT NULL DEFAULT 6,
|
||||
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// User cooldowns table
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS user_cooldowns (
|
||||
id TEXT PRIMARY KEY DEFAULT (hex(randomblob(16))),
|
||||
userId TEXT NOT NULL UNIQUE,
|
||||
lastPlacement DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
cooldownEnds DATETIME NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Create indexes
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_pixels_xy ON pixels(x, y)`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_pixels_created ON pixels(createdAt DESC)`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_cooldowns_user ON user_cooldowns(userId)`);
|
||||
}
|
||||
|
||||
private initDefaultConfig() {
|
||||
const existingConfig = this.db.prepare('SELECT * FROM canvas_config LIMIT 1').get();
|
||||
if (!existingConfig) {
|
||||
this.db.prepare(`
|
||||
INSERT INTO canvas_config (canvasWidth, canvasHeight, defaultCooldown, enableAutomaticEvents, eventDuration, eventInterval)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
config.canvasWidth,
|
||||
config.canvasHeight,
|
||||
config.defaultCooldown,
|
||||
config.enableAutomaticEvents ? 1 : 0,
|
||||
config.eventDurationMinutes,
|
||||
config.eventIntervalHours
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getPixel(x: number, y: number): Promise<Pixel | undefined> {
|
||||
const row = this.db.prepare('SELECT * FROM pixels WHERE x = ? AND y = ? ORDER BY createdAt DESC LIMIT 1').get(x, y) as any;
|
||||
if (!row) return undefined;
|
||||
|
||||
return {
|
||||
...row,
|
||||
createdAt: new Date(row.createdAt),
|
||||
enableAutomaticEvents: Boolean(row.enableAutomaticEvents)
|
||||
};
|
||||
}
|
||||
|
||||
async getAllPixels(): Promise<Pixel[]> {
|
||||
const rows = this.db.prepare(`
|
||||
SELECT p1.* FROM pixels p1
|
||||
INNER JOIN (
|
||||
SELECT x, y, MAX(createdAt) as maxCreated
|
||||
FROM pixels
|
||||
GROUP BY x, y
|
||||
) p2 ON p1.x = p2.x AND p1.y = p2.y AND p1.createdAt = p2.maxCreated
|
||||
`).all() as any[];
|
||||
|
||||
return rows.map(row => ({
|
||||
...row,
|
||||
createdAt: new Date(row.createdAt)
|
||||
}));
|
||||
}
|
||||
|
||||
async placePixel(insertPixel: InsertPixel): Promise<Pixel> {
|
||||
const id = randomUUID();
|
||||
const now = new Date();
|
||||
|
||||
this.db.prepare(`
|
||||
INSERT INTO pixels (id, x, y, color, userId, username, createdAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, insertPixel.x, insertPixel.y, insertPixel.color, insertPixel.userId, insertPixel.username, now.toISOString());
|
||||
|
||||
return {
|
||||
id,
|
||||
...insertPixel,
|
||||
createdAt: now
|
||||
};
|
||||
}
|
||||
|
||||
async getCanvasConfig(): Promise<CanvasConfig> {
|
||||
const row = this.db.prepare('SELECT * FROM canvas_config ORDER BY updatedAt DESC LIMIT 1').get() as any;
|
||||
|
||||
return {
|
||||
...row,
|
||||
enableAutomaticEvents: Boolean(row.enableAutomaticEvents),
|
||||
updatedAt: new Date(row.updatedAt)
|
||||
};
|
||||
}
|
||||
|
||||
async updateCanvasConfig(configUpdate: InsertCanvasConfig): Promise<CanvasConfig> {
|
||||
const currentConfig = await this.getCanvasConfig();
|
||||
const now = new Date();
|
||||
|
||||
this.db.prepare(`
|
||||
UPDATE canvas_config
|
||||
SET canvasWidth = ?, canvasHeight = ?, defaultCooldown = ?,
|
||||
enableAutomaticEvents = ?, eventDuration = ?, eventInterval = ?,
|
||||
updatedAt = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
configUpdate.canvasWidth ?? currentConfig.canvasWidth,
|
||||
configUpdate.canvasHeight ?? currentConfig.canvasHeight,
|
||||
configUpdate.defaultCooldown ?? currentConfig.defaultCooldown,
|
||||
configUpdate.enableAutomaticEvents ? 1 : 0,
|
||||
configUpdate.eventDuration ?? currentConfig.eventDuration,
|
||||
configUpdate.eventInterval ?? currentConfig.eventInterval,
|
||||
now.toISOString(),
|
||||
currentConfig.id
|
||||
);
|
||||
|
||||
return this.getCanvasConfig();
|
||||
}
|
||||
|
||||
async getUserCooldown(userId: string): Promise<UserCooldown | undefined> {
|
||||
const row = this.db.prepare('SELECT * FROM user_cooldowns WHERE userId = ?').get(userId) as any;
|
||||
if (!row) return undefined;
|
||||
|
||||
return {
|
||||
...row,
|
||||
lastPlacement: new Date(row.lastPlacement),
|
||||
cooldownEnds: new Date(row.cooldownEnds)
|
||||
};
|
||||
}
|
||||
|
||||
async setUserCooldown(insertCooldown: InsertUserCooldown): Promise<UserCooldown> {
|
||||
const id = randomUUID();
|
||||
const now = new Date();
|
||||
|
||||
this.db.prepare(`
|
||||
INSERT OR REPLACE INTO user_cooldowns (id, userId, lastPlacement, cooldownEnds)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(id, insertCooldown.userId, now.toISOString(), insertCooldown.cooldownEnds.toISOString());
|
||||
|
||||
return {
|
||||
id,
|
||||
userId: insertCooldown.userId,
|
||||
lastPlacement: now,
|
||||
cooldownEnds: insertCooldown.cooldownEnds
|
||||
};
|
||||
}
|
||||
|
||||
async getRecentPlacements(limit: number = 10): Promise<Pixel[]> {
|
||||
const rows = this.db.prepare(`
|
||||
SELECT * FROM pixels
|
||||
ORDER BY createdAt DESC
|
||||
LIMIT ?
|
||||
`).all(limit) as any[];
|
||||
|
||||
return rows.map(row => ({
|
||||
...row,
|
||||
createdAt: new Date(row.createdAt)
|
||||
}));
|
||||
}
|
||||
|
||||
// Canvas-Erweiterungsmethode
|
||||
async expandCanvas(newWidth: number, newHeight: number): Promise<void> {
|
||||
const currentConfig = await this.getCanvasConfig();
|
||||
|
||||
if (newWidth < currentConfig.canvasWidth || newHeight < currentConfig.canvasHeight) {
|
||||
throw new Error("Canvas kann nur erweitert werden, nicht verkleinert");
|
||||
}
|
||||
|
||||
await this.updateCanvasConfig({
|
||||
canvasWidth: newWidth,
|
||||
canvasHeight: newHeight,
|
||||
defaultCooldown: currentConfig.defaultCooldown,
|
||||
enableAutomaticEvents: currentConfig.enableAutomaticEvents,
|
||||
eventDuration: currentConfig.eventDuration,
|
||||
eventInterval: currentConfig.eventInterval
|
||||
});
|
||||
}
|
||||
|
||||
async deletePixel(pixelId: string): Promise<void> {
|
||||
this.db.prepare("DELETE FROM pixels WHERE id = ?").run(pixelId);
|
||||
}
|
||||
|
||||
async clearCanvas(): Promise<void> {
|
||||
this.db.prepare("DELETE FROM pixels").run();
|
||||
}
|
||||
}
|
||||
@@ -7,17 +7,21 @@ export interface IStorage {
|
||||
getPixel(x: number, y: number): Promise<Pixel | undefined>;
|
||||
getAllPixels(): Promise<Pixel[]>;
|
||||
placePixel(pixel: InsertPixel): Promise<Pixel>;
|
||||
|
||||
|
||||
// Config operations
|
||||
getCanvasConfig(): Promise<CanvasConfig>;
|
||||
updateCanvasConfig(config: InsertCanvasConfig): Promise<CanvasConfig>;
|
||||
|
||||
|
||||
// User cooldown operations
|
||||
getUserCooldown(userId: string): Promise<UserCooldown | undefined>;
|
||||
setUserCooldown(cooldown: InsertUserCooldown): Promise<UserCooldown>;
|
||||
|
||||
|
||||
// Recent activity
|
||||
getRecentPlacements(limit?: number): Promise<Pixel[]>;
|
||||
|
||||
// Admin operations
|
||||
deletePixel(pixelId: string): Promise<void>;
|
||||
clearCanvas(): Promise<void>;
|
||||
}
|
||||
|
||||
export class MemStorage implements IStorage {
|
||||
@@ -60,7 +64,7 @@ export class MemStorage implements IStorage {
|
||||
id,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
|
||||
this.pixels.set(this.getPixelKey(pixel.x, pixel.y), pixel);
|
||||
return pixel;
|
||||
}
|
||||
@@ -89,7 +93,7 @@ export class MemStorage implements IStorage {
|
||||
id,
|
||||
lastPlacement: new Date(),
|
||||
};
|
||||
|
||||
|
||||
this.userCooldowns.set(cooldown.userId, cooldown);
|
||||
return cooldown;
|
||||
}
|
||||
@@ -100,6 +104,21 @@ export class MemStorage implements IStorage {
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
async deletePixel(pixelId: string): Promise<void> {
|
||||
this.pixels.delete(pixelId);
|
||||
}
|
||||
|
||||
async clearCanvas(): Promise<void> {
|
||||
this.pixels.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new MemStorage();
|
||||
// The SQLiteStorage import and usage below is not part of the changes,
|
||||
// but is included to ensure the file is complete as per instructions.
|
||||
import { SQLiteStorage } from "./sqlite-storage";
|
||||
|
||||
// Verwende SQLite im Development-Modus, Memory-Storage in Production
|
||||
export const storage = process.env.NODE_ENV === 'development'
|
||||
? new SQLiteStorage('./dev-database.sqlite')
|
||||
: new MemStorage();
|
||||
Reference in New Issue
Block a user