feat: add campaign membership, invite generation, and join-by-token routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d1156745ca
commit
56d46166cd
1 changed files with 106 additions and 16 deletions
|
|
@ -1,14 +1,22 @@
|
|||
import { Router } from "express";
|
||||
import type { RowDataPacket, ResultSetHeader } from "mysql2";
|
||||
import crypto from "crypto";
|
||||
import db from "../db.js";
|
||||
import { requireAuth, requireCampaignRole } from "../auth/middleware.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/campaigns
|
||||
router.get("/", async (_req, res) => {
|
||||
// GET /api/campaigns — only campaigns current user is a member of
|
||||
router.get("/", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user!.userId;
|
||||
const [rows] = await db.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM campaigns ORDER BY created_at DESC"
|
||||
`SELECT c.*, cm.role
|
||||
FROM campaigns c
|
||||
JOIN campaign_members cm ON cm.campaign_id = c.id
|
||||
WHERE cm.user_id = ?
|
||||
ORDER BY c.created_at DESC`,
|
||||
[userId]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
|
|
@ -17,31 +25,42 @@ router.get("/", async (_req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// POST /api/campaigns
|
||||
router.post("/", async (req, res) => {
|
||||
// POST /api/campaigns — create campaign, creator becomes DM
|
||||
router.post("/", requireAuth, async (req, res) => {
|
||||
const { name } = req.body;
|
||||
if (!name?.trim()) {
|
||||
res.status(400).json({ error: "Campaign name is required" });
|
||||
return;
|
||||
}
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
const { name } = req.body;
|
||||
if (!name?.trim()) {
|
||||
res.status(400).json({ error: "Campaign name is required" });
|
||||
return;
|
||||
}
|
||||
const [result] = await db.execute<ResultSetHeader>(
|
||||
await conn.beginTransaction();
|
||||
const [result] = await conn.execute<ResultSetHeader>(
|
||||
"INSERT INTO campaigns (name) VALUES (?)",
|
||||
[name.trim()]
|
||||
);
|
||||
const campaignId = result.insertId;
|
||||
await conn.execute(
|
||||
"INSERT INTO campaign_members (campaign_id, user_id, role) VALUES (?, ?, 'dm')",
|
||||
[campaignId, req.user!.userId]
|
||||
);
|
||||
await conn.commit();
|
||||
const [rows] = await db.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM campaigns WHERE id = ?",
|
||||
[result.insertId]
|
||||
"SELECT c.*, 'dm' as role FROM campaigns c WHERE c.id = ?",
|
||||
[campaignId]
|
||||
);
|
||||
res.status(201).json(rows[0]);
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/campaigns/:id
|
||||
router.get("/:id", async (req, res) => {
|
||||
router.get("/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await db.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM campaigns WHERE id = ?",
|
||||
|
|
@ -58,8 +77,8 @@ router.get("/:id", async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// DELETE /api/campaigns/:id
|
||||
router.delete("/:id", async (req, res) => {
|
||||
// DELETE /api/campaigns/:id — DM only
|
||||
router.delete("/:id", requireAuth, requireCampaignRole("dm"), async (req, res) => {
|
||||
try {
|
||||
const [result] = await db.execute<ResultSetHeader>(
|
||||
"DELETE FROM campaigns WHERE id = ?",
|
||||
|
|
@ -76,4 +95,75 @@ router.delete("/:id", async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// GET /api/campaigns/:id/my-role — returns current user's role in this campaign
|
||||
router.get("/:id/my-role", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await db.execute<RowDataPacket[]>(
|
||||
"SELECT role FROM campaign_members WHERE campaign_id = ? AND user_id = ?",
|
||||
[req.params.id, req.user!.userId]
|
||||
);
|
||||
if (rows.length === 0) {
|
||||
res.status(403).json({ error: "Not a campaign member" });
|
||||
return;
|
||||
}
|
||||
res.json({ role: rows[0].role });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/campaigns/:id/invite — generate invite link (DM only)
|
||||
router.post("/:id/invite", requireAuth, requireCampaignRole("dm"), async (req, res) => {
|
||||
try {
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
await db.execute(
|
||||
"INSERT INTO campaign_invites (campaign_id, token, created_by) VALUES (?, ?, ?)",
|
||||
[req.params.id, token, req.user!.userId]
|
||||
);
|
||||
const clientUrl = process.env.CLIENT_URL ?? "http://localhost:5173";
|
||||
res.json({ url: `${clientUrl}/join/${token}` });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/campaigns/join/:token — join campaign as player
|
||||
router.post("/join/:token", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const [inviteRows] = await db.execute<RowDataPacket[]>(
|
||||
`SELECT * FROM campaign_invites
|
||||
WHERE token = ? AND (expires_at IS NULL OR expires_at > NOW())`,
|
||||
[req.params.token]
|
||||
);
|
||||
if (inviteRows.length === 0) {
|
||||
res.status(404).json({ error: "Invite not found or expired" });
|
||||
return;
|
||||
}
|
||||
|
||||
const campaignId = inviteRows[0].campaign_id as number;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
// Already a member? Return 200 silently
|
||||
const [existing] = await db.execute<RowDataPacket[]>(
|
||||
"SELECT role FROM campaign_members WHERE campaign_id = ? AND user_id = ?",
|
||||
[campaignId, userId]
|
||||
);
|
||||
if (existing.length > 0) {
|
||||
res.json({ campaignId, role: existing[0].role });
|
||||
return;
|
||||
}
|
||||
|
||||
await db.execute(
|
||||
"INSERT INTO campaign_members (campaign_id, user_id, role) VALUES (?, ?, 'player')",
|
||||
[campaignId, userId]
|
||||
);
|
||||
res.json({ campaignId, role: "player" });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue