137 lines
5.7 KiB
TypeScript
137 lines
5.7 KiB
TypeScript
import { Router } from "express";
|
|
import { z } from "zod";
|
|
import { db } from "../db/client.js";
|
|
import { requireAuth, type AuthedRequest } from "../middleware.js";
|
|
|
|
const characterSchema = z.object({
|
|
campaignId: z.number().int(),
|
|
characterName: z.string().min(1),
|
|
race: z.string().min(1),
|
|
class: z.string().min(1),
|
|
level: z.number().int().min(1).max(20),
|
|
alignment: z.string().optional(),
|
|
pronouns: z.string().optional(),
|
|
portraitUrl: z.string().url().optional(),
|
|
bio: z.string().optional(),
|
|
notes: z.string().optional()
|
|
});
|
|
|
|
export const characterRoutes = Router();
|
|
|
|
characterRoutes.post("/characters", requireAuth, (req: AuthedRequest, res) => {
|
|
const parsed = characterSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
res.status(400).json({ error: parsed.error.flatten() });
|
|
return;
|
|
}
|
|
const campaign = db
|
|
.prepare("SELECT id FROM campaigns WHERE id = ? AND guild_id = ?")
|
|
.get(parsed.data.campaignId, req.auth!.guildId);
|
|
if (!campaign) {
|
|
res.status(404).json({ error: "Campaign not found" });
|
|
return;
|
|
}
|
|
const result = db
|
|
.prepare(
|
|
`INSERT INTO characters
|
|
(campaign_id, user_id, character_name, race, class, level, alignment, pronouns, portrait_url, bio, notes, is_active)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)`
|
|
)
|
|
.run(
|
|
parsed.data.campaignId,
|
|
req.auth!.userId,
|
|
parsed.data.characterName,
|
|
parsed.data.race,
|
|
parsed.data.class,
|
|
parsed.data.level,
|
|
parsed.data.alignment ?? null,
|
|
parsed.data.pronouns ?? null,
|
|
parsed.data.portraitUrl ?? null,
|
|
parsed.data.bio ?? null,
|
|
parsed.data.notes ?? null
|
|
);
|
|
res.status(201).json({ id: Number(result.lastInsertRowid) });
|
|
});
|
|
|
|
characterRoutes.get("/characters/mine", requireAuth, (req: AuthedRequest, res) => {
|
|
const characters = db.prepare(
|
|
`SELECT c.id, c.character_name, c.class, c.race, c.level, c.alignment,
|
|
c.portrait_url, c.bio, c.notes, c.stats_json, c.dndbeyond_id, c.dndbeyond_last_sync,
|
|
c.is_active, camp.name AS campaign_name, camp.id AS campaign_id
|
|
FROM characters c
|
|
JOIN campaigns camp ON camp.id = c.campaign_id
|
|
WHERE c.user_id = ? AND camp.guild_id = ?
|
|
ORDER BY camp.name ASC, c.character_name ASC`
|
|
).all(req.auth!.userId, req.auth!.guildId);
|
|
res.json(characters);
|
|
});
|
|
|
|
characterRoutes.get("/characters/:id", requireAuth, (req: AuthedRequest, res) => {
|
|
const id = Number(req.params.id);
|
|
const char = db.prepare(
|
|
`SELECT c.id, c.character_name, c.class, c.race, c.level, c.alignment, c.pronouns,
|
|
c.portrait_url, c.bio, c.notes, c.stats_json, c.dndbeyond_id, c.dndbeyond_last_sync,
|
|
c.is_active, c.user_id, camp.name AS campaign_name, camp.id AS campaign_id
|
|
FROM characters c
|
|
JOIN campaigns camp ON camp.id = c.campaign_id
|
|
WHERE c.id = ? AND camp.guild_id = ?`
|
|
).get(id, req.auth!.guildId);
|
|
if (!char) { res.status(404).json({ error: "Character not found" }); return; }
|
|
res.json(char);
|
|
});
|
|
|
|
const patchCharacterSchema = z.object({
|
|
characterName: z.string().min(1).max(100).optional(),
|
|
race: z.string().min(1).max(100).optional(),
|
|
class: z.string().min(1).max(100).optional(),
|
|
level: z.number().int().min(1).max(20).optional(),
|
|
alignment: z.string().max(50).nullable().optional(),
|
|
pronouns: z.string().max(50).nullable().optional(),
|
|
bio: z.string().nullable().optional(),
|
|
notes: z.string().nullable().optional(),
|
|
});
|
|
|
|
characterRoutes.patch("/characters/:id", requireAuth, (req: AuthedRequest, res) => {
|
|
const id = Number(req.params.id);
|
|
const parsed = patchCharacterSchema.safeParse(req.body);
|
|
if (!parsed.success) { res.status(400).json({ error: parsed.error.flatten() }); return; }
|
|
|
|
const char = db.prepare("SELECT id, user_id FROM characters WHERE id = ?").get(id) as { id: number; user_id: number } | undefined;
|
|
if (!char || char.user_id !== req.auth!.userId) { res.status(404).json({ error: "Character not found" }); return; }
|
|
|
|
const d = parsed.data;
|
|
const sets: string[] = [];
|
|
const vals: unknown[] = [];
|
|
|
|
if (d.characterName !== undefined) { sets.push("character_name = ?"); vals.push(d.characterName); }
|
|
if (d.race !== undefined) { sets.push("race = ?"); vals.push(d.race); }
|
|
if (d.class !== undefined) { sets.push("class = ?"); vals.push(d.class); }
|
|
if (d.level !== undefined) { sets.push("level = ?"); vals.push(d.level); }
|
|
if (d.alignment !== undefined) { sets.push("alignment = ?"); vals.push(d.alignment); }
|
|
if (d.pronouns !== undefined) { sets.push("pronouns = ?"); vals.push(d.pronouns); }
|
|
if (d.bio !== undefined) { sets.push("bio = ?"); vals.push(d.bio); }
|
|
if (d.notes !== undefined) { sets.push("notes = ?"); vals.push(d.notes); }
|
|
|
|
if (sets.length === 0) { res.json({ ok: true }); return; }
|
|
vals.push(id);
|
|
db.prepare(`UPDATE characters SET ${sets.join(", ")} WHERE id = ?`).run(...vals);
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
characterRoutes.post("/characters/:id/activate", requireAuth, (req: AuthedRequest, res) => {
|
|
const id = Number(req.params.id);
|
|
const character = db
|
|
.prepare("SELECT id, campaign_id, user_id FROM characters WHERE id = ?")
|
|
.get(id) as { id: number; campaign_id: number; user_id: number } | undefined;
|
|
if (!character || character.user_id !== req.auth!.userId) {
|
|
res.status(404).json({ error: "Character not found" });
|
|
return;
|
|
}
|
|
db.prepare("UPDATE characters SET is_active = 0 WHERE campaign_id = ? AND user_id = ?").run(
|
|
character.campaign_id,
|
|
req.auth!.userId
|
|
);
|
|
db.prepare("UPDATE characters SET is_active = 1 WHERE id = ?").run(id);
|
|
res.json({ ok: true });
|
|
});
|
|
|