Skip to content

Commit

Permalink
Finishes GET /player/(uuid) API, adds changing privacy settings (POST…
Browse files Browse the repository at this point in the history
… /server/privacy).
  • Loading branch information
aelithron committed Dec 14, 2024
1 parent 73a6bb7 commit b824235
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/db/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mysqlTable, varchar, text, timestamp, boolean } from "drizzle-orm/mysql-core";

export const playerStats = mysqlTable("player_stats", {
id: varchar("id", { length: 300 }).notNull().primaryKey(), // Combo ID, used to write data avoiding duplicates
id: varchar("id", { length: 300 }).notNull(), // Combo ID, used to write data avoiding duplicates
uuid: varchar("uuid", { length: 36 }).notNull(),
statName: varchar("stat_name", { length: 255 }).notNull(),
statValue: text("stat_value").notNull(),
Expand Down
21 changes: 19 additions & 2 deletions src/db/stats.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'dotenv/config';
import { drizzle } from "drizzle-orm/mysql2";
import { playerStats } from "./schema";
import { StatArray } from "../server";
import { StatArray, PublicSettings } from "../server";
import { eq } from 'drizzle-orm';
import mysql from "mysql2/promise";
import { UserStats } from '../player';

import { UUIDTypes } from 'uuid';

const poolConnection = mysql.createPool({
uri: process.env.DATABASE_URL
Expand Down Expand Up @@ -78,4 +78,21 @@ export async function readStats(uuid: string): Promise<UserStats | null> {
console.error("[db] Database error: " + e)
return null;
}
}

export async function setStatPrivacy(uuid: UUIDTypes, stats: PublicSettings): Promise<boolean> {
if (process.env.NODE_ENV === "test") return true;
try {
for (const setting of stats) {
await db
.update(playerStats)
.set({ public: setting.public })
.where(eq(playerStats.id, `${setting.stat}-${uuid}`))
.execute();
}
return true;
} catch (e) {
console.error("[db] Database error: " + e)
return false;
}
}
27 changes: 22 additions & 5 deletions src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const router = express.Router();
import { readStats } from './db/stats';

export type UserStats = { statID: string, value: string, timestamp: Date, public: boolean, error?: string }[];
type PublicStat = { [id: string]: { value: string, lastUpdated: Date } };

router.get('/:id', async (req: Request, res: Response) => {
const player: string = req.params.id;
Expand All @@ -15,14 +16,30 @@ router.get('/:id', async (req: Request, res: Response) => {
return;
}
const stats: UserStats | null = await readStats(player);
if (stats === null && process.env.NODE_ENV !== "test") {
if (stats === null) {
if (process.env.NODE_ENV === "test") {
res.json({ uuid: escapeHtml(player) });
return;
}
res.status(500);
res.json({ error: "UNKNOWN_ERROR", message: "An unknown error occurred with the database." });
res.json({ error: "UNKNOWN", message: "An unknown error occurred with the database." });
return;
}
if (stats === null && process.env.NODE_ENV === "test") {
res.json({ uuid: escapeHtml(player) });

const publicStats: PublicStat = {};
for (const stat of stats) {
if (stat.public) {
publicStats[stat.statID] = {
value: stat.value,
lastUpdated: stat.timestamp
}
}
}

if (Object.keys(publicStats).length === 0) {
res.status(404);
res.json({ error: "NO_PUBLIC_STATS", message: "The requested player has no public stats." });
return;
}
res.json({ uuid: escapeHtml(player) });
res.json({ uuid: escapeHtml(player), stats: publicStats });
})
64 changes: 60 additions & 4 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import express, { Request, Response } from 'express';
import { UUIDTypes, validate as uuidValidate } from 'uuid';
import { writeStats } from './db/stats';
import { writeStats, setStatPrivacy } from './db/stats';
export const router = express.Router();

export type StatArray = { uuid: UUIDTypes; value: string }[];
export type StatArray = { uuid: UUIDTypes; value: string; }[];
export type PublicSettings = { stat: string; public: boolean; }[];

router.post('/stats', async (req: Request, res: Response) => {
res.contentType("application/json");
Expand Down Expand Up @@ -43,7 +44,7 @@ router.post('/stats', async (req: Request, res: Response) => {
res.json({ error: "INVALID_RECORD_UUID", message: "An invalid record UUID was entered." });
return;
}
if (record.value === undefined) {
if (record.value === undefined || typeof record.value !== "string") {
res.status(400);
res.json({ error: "INVALID_RECORD_VALUE", message: "An invalid record value was entered." });
return;
Expand All @@ -54,8 +55,63 @@ router.post('/stats', async (req: Request, res: Response) => {
const success: boolean = await writeStats(statID, records);
if (!success) {
res.status(500);
res.json({ error: "UNKNOWN_ERROR", message: "An unknown error occurred with the database." });
res.json({ error: "UNKNOWN", message: "An unknown error occurred with the database." });
return;
}
res.json({ message: "Stats updated." });
})

router.post('/stats/privacy', async (req: Request, res: Response) => {
res.contentType("application/json");
if (req.headers['authorization'] === undefined) {
res.status(401);
res.json({ error: "UNAUTHORIZED", message: "No API key was entered." });
return;
}
if (req.headers['authorization'] !== `Bearer ${process.env.API_KEY}`) {
res.status(403);
res.json({ error: "FORBIDDEN", message: "An invalid API key was entered." });
return;
}
const body = req.body;
const serverID: string = body.serverID;
if (serverID === undefined) {
res.status(400);
res.json({ error: "INVALID_BODY", message: "An invalid server ID was entered." });
return;
}
if (body.uuid === undefined || !uuidValidate(body.uuid)) {
res.status(400);
res.json({ error: "INVALID_BODY", message: "An invalid UUID was entered." });
return;
}
const settings: PublicSettings = body.settings;
if (settings === undefined || settings === null || settings.length === 0) {
res.status(400);
res.json({ error: "INVALID_BODY", message: "An invalid settings array was entered." });
return;
}
// Settings structure verification (I decided to reject the whole request if even one part is invalid)
for (const setting of settings) {
if (setting.stat === undefined || typeof setting.stat !== "string") {
res.status(400);
res.json({ error: "INVALID_SETTING_STATID", message: "An invalid setting stat ID was entered (must be a valid string)." });
return;
}
if (setting.public === undefined || typeof setting.public !== "boolean") {
res.status(400);
res.json({ error: "INVALID_SETTING_PUBLIC", message: "An invalid setting public value was entered (must be a boolean)." });
return;
}
}

// Note: Currently, this does not check if a stat is valid.
// It doesn't break anything this way though, as the "where" matches zero stats if it's invalid.
const result = await setStatPrivacy(body.uuid, settings);
if (!result) {
res.status(500);
res.json({ error: "UNKNOWN", message: "An unknown error occurred with the database." });
return;
}
res.json({ message: "Privacy settings updated." });
})

0 comments on commit b824235

Please sign in to comment.