Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions api/src/repositories/mapObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import pool from "../db";

export const MAP_SIZE = 100;

export type MapObjectState = "placed" | "structure" | "door_open" | "door_closed" | "sign";

export type MapObjectRecord = {
id: number;
mapId: number;
x: number;
y: number;
objIndex: number;
amount: number;
state: MapObjectState;
createdBy: string | null;
createdAt: string;
};

export type StructureTileInput = {
x: number;
y: number;
objIndex: number;
amount?: number;
state?: MapObjectState;
};

function validateCoordinates(x: number, y: number): void {
if (x < 1 || x > MAP_SIZE || y < 1 || y > MAP_SIZE) {
throw new Error(`Coordenadas fuera de rango (1-${MAP_SIZE}): (${x}, ${y})`);
}
}
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Outdated

export async function placeObject(
mapId: number,
x: number,
y: number,
objIndex: number,
amount: number = 1,
createdBy: string | null = null,
state: MapObjectState = "placed"
): Promise<MapObjectRecord> {
validateCoordinates(x, y);
if (objIndex <= 0) {
throw new Error("objIndex debe ser mayor a 0");
}

const res = await pool.query(
`INSERT INTO game_map_objects (map_id, x, y, obj_index, amount, state, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, map_id AS "mapId", x, y, obj_index AS "objIndex", amount, state, created_by AS "createdBy", created_at AS "createdAt"`,
[mapId, x, y, objIndex, amount, state, createdBy]
);
Comment on lines +54 to +73

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: state is not validated against allowed MapObjectState values

placeObject, setObjectState, and placeStructure write the client-supplied state directly to the DB. MapObjectState is a compile-time-only TypeScript union, so any arbitrary string from the request body is accepted and persisted, corrupting object state semantics (door_open/door_closed/etc.). Validate state against an explicit runtime allow-list (or add a CHECK constraint / enum on the column) and reject unknown values.

Was this helpful? React with 👍 / 👎

return res.rows[0];
Comment thread
gitar-bot[bot] marked this conversation as resolved.
}

export async function moveObject(id: number, newX: number, newY: number): Promise<MapObjectRecord | null> {
validateCoordinates(newX, newY);
const res = await pool.query(
`UPDATE game_map_objects
SET x = $1, y = $2
WHERE id = $3
RETURNING id, map_id AS "mapId", x, y, obj_index AS "objIndex", amount, state, created_by AS "createdBy", created_at AS "createdAt"`,
[newX, newY, id]
);
return res.rows[0] || null;
}

export async function removeObject(id: number): Promise<boolean> {
const res = await pool.query(`DELETE FROM game_map_objects WHERE id = $1`, [id]);
return (res.rowCount ?? 0) > 0;
}

export async function getMapObjects(mapId: number): Promise<MapObjectRecord[]> {
const res = await pool.query(
`SELECT id, map_id AS "mapId", x, y, obj_index AS "objIndex", amount, state, created_by AS "createdBy", created_at AS "createdAt"
FROM game_map_objects
WHERE map_id = $1
ORDER BY id ASC`,
[mapId]
);
return res.rows;
}

export async function setObjectState(id: number, state: MapObjectState): Promise<MapObjectRecord | null> {
const res = await pool.query(
`UPDATE game_map_objects
SET state = $1
WHERE id = $2
RETURNING id, map_id AS "mapId", x, y, obj_index AS "objIndex", amount, state, created_by AS "createdBy", created_at AS "createdAt"`,
[state, id]
);
return res.rows[0] || null;
}

export async function placeStructure(
mapId: number,
tiles: StructureTileInput[],
createdBy: string | null = null
): Promise<MapObjectRecord[]> {
if (!tiles.length) return [];

const client = await pool.connect();
try {
await client.query("BEGIN");
const results: MapObjectRecord[] = [];
for (const tile of tiles) {
validateCoordinates(tile.x, tile.y);
if (tile.objIndex <= 0) {
throw new Error("objIndex debe ser mayor a 0");
}
const res = await client.query(
`INSERT INTO game_map_objects (map_id, x, y, obj_index, amount, state, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, map_id AS "mapId", x, y, obj_index AS "objIndex", amount, state, created_by AS "createdBy", created_at AS "createdAt"`,
[mapId, tile.x, tile.y, tile.objIndex, tile.amount ?? 1, tile.state ?? "structure", createdBy]
);
results.push(res.rows[0]);
}
await client.query("COMMIT");
return results;
} catch (err) {
await client.query("ROLLBACK");
throw err;
} finally {
client.release();
}
}
89 changes: 89 additions & 0 deletions api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2891,4 +2891,93 @@ app.get("/user-online-stats", async (request, response) => {
}
});

app.get("/internal/map-objects/:mapId", requireAuth, async (request, response) => {
try {
const mapId = Number(request.params.mapId);
const { getMapObjects } = await import("./repositories/mapObjects");
const result = await getMapObjects(mapId);
response.json(result);
} catch (error) {
response.status(500).json({
error: error instanceof Error ? error.message : "Unexpected error",
});
}
});

app.post("/internal/map-objects", requireAuth, async (request, response) => {
try {
const { mapId, x, y, objIndex, amount, createdBy, state } = request.body;
const { placeObject } = await import("./repositories/mapObjects");
const result = await placeObject(mapId, x, y, objIndex, amount, createdBy, state);
response.status(201).json(result);
} catch (error) {
response.status(400).json({
error: error instanceof Error ? error.message : "Unexpected error",
});
}
});
Comment on lines +2907 to +2918

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Security: Editor write endpoints use static-token requireAuth, not admin auth

The new /internal/map-objects write endpoints (place/move/delete/state/structure) mutate map/world data but are guarded only by requireAuth, which just compares the Authorization header to a single static config.tokenAuth. Comparable game-data/map-editing mutations in this server use requireAdminEmailSession + isAuthorizedGameDataAdmin. If these endpoints are reachable by regular clients (not only trusted server-to-server callers), anyone holding the shared token can place/move/delete world objects. Confirm the intended caller; if end users can reach these routes, switch to admin session authorization.

Was this helpful? React with 👍 / 👎


app.put("/internal/map-objects/:id/move", requireAuth, async (request, response) => {
try {
const id = Number(request.params.id);
const { newX, newY } = request.body;
const { moveObject } = await import("./repositories/mapObjects");
const result = await moveObject(id, newX, newY);
if (!result) {
return response.status(404).json({ error: "Objeto no encontrado" });
}
response.json(result);
} catch (error) {
response.status(400).json({
error: error instanceof Error ? error.message : "Unexpected error",
});
}
});

app.delete("/internal/map-objects/:id", requireAuth, async (request, response) => {
try {
const id = Number(request.params.id);
const { removeObject } = await import("./repositories/mapObjects");
const ok = await removeObject(id);
if (!ok) {
return response.status(404).json({ error: "Objeto no encontrado" });
}
response.json({ success: true });
} catch (error) {
response.status(500).json({
error: error instanceof Error ? error.message : "Unexpected error",
});
}
});

app.put("/internal/map-objects/:id/state", requireAuth, async (request, response) => {
try {
const id = Number(request.params.id);
const { state } = request.body;
const { setObjectState } = await import("./repositories/mapObjects");
const result = await setObjectState(id, state);
if (!result) {
return response.status(404).json({ error: "Objeto no encontrado" });
}
response.json(result);
} catch (error) {
response.status(400).json({
error: error instanceof Error ? error.message : "Unexpected error",
});
}
});

app.post("/internal/map-objects/structure", requireAuth, async (request, response) => {
try {
const { mapId, tiles, createdBy } = request.body;
const { placeStructure } = await import("./repositories/mapObjects");
const result = await placeStructure(mapId, tiles, createdBy);
response.status(201).json(result);
} catch (error) {
response.status(400).json({
error: error instanceof Error ? error.message : "Unexpected error",
});
}
});

void start();