/** * GameForge Routes * API endpoints for game development project management * Ported from aethex-forge */ import { Router, Request, Response } from "express"; import { supabase } from "./supabase.js"; import { requireAuth } from "./auth.js"; const router = Router(); // Helper to get user ID from session function getUserId(req: Request): string | null { return (req.session as any)?.userId || null; } // ==================== PROJECTS ROUTES ==================== /** * GET /api/gameforge/projects - List all projects * GET /api/gameforge/projects?id=xxx - Get single project */ router.get("/projects", requireAuth, async (req: Request, res: Response) => { try { const { id, status, platform, limit = 50, offset = 0 } = req.query; if (id) { // Get single project with full details const { data, error } = await supabase .from("gameforge_projects") .select(` *, gameforge_team_members(*) `) .eq("id", id) .single(); if (error) throw error; if (!data) return res.status(404).json({ error: "Project not found" }); return res.json(data); } // List all projects with filters let query = supabase .from("gameforge_projects") .select(` id, name, description, status, platform, genre, target_release_date, actual_release_date, team_size, budget, current_spend, lead_id, created_at `, { count: "exact" }); if (status) query = query.eq("status", status as string); if (platform) query = query.eq("platform", platform as string); const { data, error, count } = await query .order("created_at", { ascending: false }) .range(Number(offset), Number(offset) + Number(limit) - 1); if (error) throw error; res.json({ projects: data || [], total: count || 0, limit: Number(limit), offset: Number(offset) }); } catch (err: any) { console.error("[GameForge Projects] List error:", err); res.status(500).json({ error: err.message }); } }); /** * POST /api/gameforge/projects - Create a new project */ router.post("/projects", requireAuth, async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: "Unauthorized" }); const { name, description, platform, genre, target_release_date, budget, repository_url, documentation_url } = req.body; if (!name || !platform) { return res.status(400).json({ error: "name and platform are required" }); } const { data, error } = await supabase .from("gameforge_projects") .insert({ name, description, status: "planning", lead_id: userId, platform, genre: genre || [], target_release_date, budget, repository_url, documentation_url }) .select() .single(); if (error) throw error; res.status(201).json(data); } catch (err: any) { console.error("[GameForge Projects] Create error:", err); res.status(500).json({ error: err.message }); } }); /** * PUT /api/gameforge/projects/:id - Update a project */ router.put("/projects/:id", requireAuth, async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: "Unauthorized" }); const { id } = req.params; const { name, description, status, platform, genre, target_release_date, actual_release_date, budget, current_spend, repository_url, documentation_url } = req.body; // Verify user is project lead const { data: project, error: checkError } = await supabase .from("gameforge_projects") .select("lead_id") .eq("id", id) .single(); if (checkError || !project) { return res.status(404).json({ error: "Project not found" }); } if (project.lead_id !== userId) { return res.status(403).json({ error: "Only project lead can update" }); } const updateData: any = {}; if (name !== undefined) updateData.name = name; if (description !== undefined) updateData.description = description; if (status !== undefined) updateData.status = status; if (platform !== undefined) updateData.platform = platform; if (genre !== undefined) updateData.genre = genre; if (target_release_date !== undefined) updateData.target_release_date = target_release_date; if (actual_release_date !== undefined) updateData.actual_release_date = actual_release_date; if (budget !== undefined) updateData.budget = budget; if (current_spend !== undefined) updateData.current_spend = current_spend; if (repository_url !== undefined) updateData.repository_url = repository_url; if (documentation_url !== undefined) updateData.documentation_url = documentation_url; const { data, error } = await supabase .from("gameforge_projects") .update(updateData) .eq("id", id) .select() .single(); if (error) throw error; res.json(data); } catch (err: any) { console.error("[GameForge Projects] Update error:", err); res.status(500).json({ error: err.message }); } }); // ==================== TEAM MEMBERS ROUTES ==================== /** * GET /api/gameforge/team - List team members */ router.get("/team", requireAuth, async (req: Request, res: Response) => { try { const { user_id, project_id, role, limit = 50, offset = 0 } = req.query; let query = supabase .from("gameforge_team_members") .select("*", { count: "exact" }); if (user_id) { const { data, error } = await supabase .from("gameforge_team_members") .select("*") .eq("user_id", user_id) .single(); if (error && error.code !== "PGRST116") throw error; return res.json({ member: data }); } if (project_id) query = query.contains("project_ids", [project_id]); if (role) query = query.eq("role", role as string); query = query.eq("is_active", true); const { data, error, count } = await query .order("joined_date", { ascending: false }) .range(Number(offset), Number(offset) + Number(limit) - 1); if (error) throw error; res.json({ members: data || [], total: count || 0, limit: Number(limit), offset: Number(offset) }); } catch (err: any) { console.error("[GameForge Team] List error:", err); res.status(500).json({ error: err.message }); } }); /** * POST /api/gameforge/team - Add team member */ router.post("/team", requireAuth, async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: "Unauthorized" }); const { user_id, role, position, contract_type, hourly_rate, skills, bio, project_ids } = req.body; if (!user_id || !role) { return res.status(400).json({ error: "user_id and role are required" }); } const { data, error } = await supabase .from("gameforge_team_members") .insert({ user_id, role, position, contract_type: contract_type || "contractor", hourly_rate, skills: skills || [], bio, project_ids: project_ids || [], is_active: true }) .select() .single(); if (error) throw error; res.status(201).json(data); } catch (err: any) { console.error("[GameForge Team] Create error:", err); res.status(500).json({ error: err.message }); } }); /** * PUT /api/gameforge/team/:id - Update team member */ router.put("/team/:id", requireAuth, async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: "Unauthorized" }); const { id } = req.params; const { role, position, contract_type, hourly_rate, skills, bio, project_ids, is_active } = req.body; const updateData: any = {}; if (role !== undefined) updateData.role = role; if (position !== undefined) updateData.position = position; if (contract_type !== undefined) updateData.contract_type = contract_type; if (hourly_rate !== undefined) updateData.hourly_rate = hourly_rate; if (skills !== undefined) updateData.skills = skills; if (bio !== undefined) updateData.bio = bio; if (project_ids !== undefined) updateData.project_ids = project_ids; if (is_active !== undefined) { updateData.is_active = is_active; if (is_active === false) updateData.left_date = new Date().toISOString(); } const { data, error } = await supabase .from("gameforge_team_members") .update(updateData) .eq("id", id) .select() .single(); if (error) throw error; res.json(data); } catch (err: any) { console.error("[GameForge Team] Update error:", err); res.status(500).json({ error: err.message }); } }); // ==================== BUILDS ROUTES ==================== /** * GET /api/gameforge/builds - List builds for a project */ router.get("/builds", requireAuth, async (req: Request, res: Response) => { try { const { id, project_id, build_type, limit = 50, offset = 0 } = req.query; if (id) { const { data, error } = await supabase .from("gameforge_builds") .select(` *, gameforge_projects(id, name, platform) `) .eq("id", id) .single(); if (error) throw error; if (!data) return res.status(404).json({ error: "Build not found" }); return res.json(data); } if (!project_id) { return res.status(400).json({ error: "project_id is required" }); } let query = supabase .from("gameforge_builds") .select(` *, gameforge_projects(id, name) `, { count: "exact" }) .eq("project_id", project_id); if (build_type) query = query.eq("build_type", build_type as string); const { data, error, count } = await query .order("release_date", { ascending: false }) .range(Number(offset), Number(offset) + Number(limit) - 1); if (error) throw error; res.json({ builds: data || [], total: count || 0, limit: Number(limit), offset: Number(offset) }); } catch (err: any) { console.error("[GameForge Builds] List error:", err); res.status(500).json({ error: err.message }); } }); /** * POST /api/gameforge/builds - Create a new build */ router.post("/builds", requireAuth, async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: "Unauthorized" }); const { project_id, version, build_type, download_url, changelog, file_size, target_platforms } = req.body; if (!project_id || !version || !build_type) { return res.status(400).json({ error: "project_id, version, and build_type are required" }); } // Verify user is project lead const { data: project } = await supabase .from("gameforge_projects") .select("lead_id") .eq("id", project_id) .single(); if (project?.lead_id !== userId) { return res.status(403).json({ error: "Only project lead can create builds" }); } const { data, error } = await supabase .from("gameforge_builds") .insert({ project_id, version, build_type, download_url, changelog, file_size, target_platforms: target_platforms || [], created_by: userId }) .select() .single(); if (error) throw error; res.status(201).json(data); } catch (err: any) { console.error("[GameForge Builds] Create error:", err); res.status(500).json({ error: err.message }); } }); /** * PUT /api/gameforge/builds/:id - Update a build */ router.put("/builds/:id", requireAuth, async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: "Unauthorized" }); const { id } = req.params; const { version, build_type, download_url, changelog, file_size } = req.body; // Verify user is project lead const { data: build } = await supabase .from("gameforge_builds") .select("project_id, gameforge_projects(lead_id)") .eq("id", id) .single(); if ((build?.gameforge_projects as any)?.lead_id !== userId) { return res.status(403).json({ error: "Only project lead can update builds" }); } const updateData: any = {}; if (version !== undefined) updateData.version = version; if (build_type !== undefined) updateData.build_type = build_type; if (download_url !== undefined) updateData.download_url = download_url; if (changelog !== undefined) updateData.changelog = changelog; if (file_size !== undefined) updateData.file_size = file_size; const { data, error } = await supabase .from("gameforge_builds") .update(updateData) .eq("id", id) .select() .single(); if (error) throw error; res.json(data); } catch (err: any) { console.error("[GameForge Builds] Update error:", err); res.status(500).json({ error: err.message }); } }); // ==================== SPRINTS ROUTES ==================== /** * GET /api/gameforge/sprints - List sprints for a project */ router.get("/sprints", requireAuth, async (req: Request, res: Response) => { try { const { project_id, phase, status, limit = 50, offset = 0 } = req.query; if (!project_id) { return res.status(400).json({ error: "project_id is required" }); } let query = supabase .from("gameforge_sprints") .select("*", { count: "exact" }) .eq("project_id", project_id); if (phase) query = query.eq("phase", phase as string); if (status) query = query.eq("status", status as string); const { data, error, count } = await query .order("sprint_number", { ascending: false }) .range(Number(offset), Number(offset) + Number(limit) - 1); if (error) throw error; res.json({ sprints: data || [], total: count || 0, limit: Number(limit), offset: Number(offset) }); } catch (err: any) { console.error("[GameForge Sprints] List error:", err); res.status(500).json({ error: err.message }); } }); /** * POST /api/gameforge/sprints - Create a new sprint */ router.post("/sprints", requireAuth, async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: "Unauthorized" }); const { project_id, sprint_number, title, description, goal, start_date, end_date, planned_velocity } = req.body; if (!project_id || !sprint_number || !title) { return res.status(400).json({ error: "project_id, sprint_number, and title are required" }); } // Verify user is project lead const { data: project } = await supabase .from("gameforge_projects") .select("lead_id") .eq("id", project_id) .single(); if (project?.lead_id !== userId) { return res.status(403).json({ error: "Only project lead can create sprints" }); } const { data, error } = await supabase .from("gameforge_sprints") .insert({ project_id, sprint_number, title, description, goal, start_date, end_date, planned_velocity, created_by: userId }) .select() .single(); if (error) throw error; res.status(201).json(data); } catch (err: any) { console.error("[GameForge Sprints] Create error:", err); res.status(500).json({ error: err.message }); } }); /** * PUT /api/gameforge/sprints/:id - Update a sprint */ router.put("/sprints/:id", requireAuth, async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: "Unauthorized" }); const { id } = req.params; const { title, description, phase, status, goal, start_date, end_date, planned_velocity, actual_velocity } = req.body; // Verify user is project lead const { data: sprint } = await supabase .from("gameforge_sprints") .select("project_id, gameforge_projects(lead_id)") .eq("id", id) .single(); if ((sprint?.gameforge_projects as any)?.lead_id !== userId) { return res.status(403).json({ error: "Only project lead can update sprints" }); } const updateData: any = {}; if (title !== undefined) updateData.title = title; if (description !== undefined) updateData.description = description; if (phase !== undefined) updateData.phase = phase; if (status !== undefined) updateData.status = status; if (goal !== undefined) updateData.goal = goal; if (start_date !== undefined) updateData.start_date = start_date; if (end_date !== undefined) updateData.end_date = end_date; if (planned_velocity !== undefined) updateData.planned_velocity = planned_velocity; if (actual_velocity !== undefined) updateData.actual_velocity = actual_velocity; const { data, error } = await supabase .from("gameforge_sprints") .update(updateData) .eq("id", id) .select() .single(); if (error) throw error; res.json(data); } catch (err: any) { console.error("[GameForge Sprints] Update error:", err); res.status(500).json({ error: err.message }); } }); // ==================== TASKS ROUTES ==================== /** * GET /api/gameforge/tasks - List tasks */ router.get("/tasks", requireAuth, async (req: Request, res: Response) => { try { const { sprint_id, project_id, status, assigned_to, limit = 100, offset = 0 } = req.query; let query = supabase .from("gameforge_tasks") .select("*", { count: "exact" }); if (sprint_id) query = query.eq("sprint_id", sprint_id); if (project_id) query = query.eq("project_id", project_id); if (status) query = query.eq("status", status as string); if (assigned_to) query = query.eq("assigned_to", assigned_to); const { data, error, count } = await query .order("created_at", { ascending: false }) .range(Number(offset), Number(offset) + Number(limit) - 1); if (error) throw error; res.json({ tasks: data || [], total: count || 0, limit: Number(limit), offset: Number(offset) }); } catch (err: any) { console.error("[GameForge Tasks] List error:", err); res.status(500).json({ error: err.message }); } }); /** * POST /api/gameforge/tasks - Create a task */ router.post("/tasks", requireAuth, async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: "Unauthorized" }); const { sprint_id, project_id, title, description, priority, estimated_hours, assigned_to, due_date } = req.body; if (!project_id || !title) { return res.status(400).json({ error: "project_id and title are required" }); } const { data, error } = await supabase .from("gameforge_tasks") .insert({ sprint_id: sprint_id || null, project_id, title, description, priority: priority || "medium", estimated_hours, assigned_to: assigned_to || null, created_by: userId, due_date: due_date || null, status: "todo" }) .select() .single(); if (error) throw error; res.status(201).json(data); } catch (err: any) { console.error("[GameForge Tasks] Create error:", err); res.status(500).json({ error: err.message }); } }); /** * PUT /api/gameforge/tasks/:id - Update a task */ router.put("/tasks/:id", requireAuth, async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: "Unauthorized" }); const { id } = req.params; const { title, description, status, priority, estimated_hours, actual_hours, assigned_to, due_date } = req.body; // Verify user can edit (task creator, assignee, or project lead) const { data: task, error: taskError } = await supabase .from("gameforge_tasks") .select("project_id, assigned_to, created_by") .eq("id", id) .single(); if (taskError || !task) { return res.status(404).json({ error: "Task not found" }); } const { data: project } = await supabase .from("gameforge_projects") .select("lead_id") .eq("id", task.project_id) .single(); if ( task.assigned_to !== userId && task.created_by !== userId && project?.lead_id !== userId ) { return res.status(403).json({ error: "No permission to edit task" }); } const updateData: any = {}; if (title !== undefined) updateData.title = title; if (description !== undefined) updateData.description = description; if (status !== undefined) { updateData.status = status; if (status === "done") updateData.completed_at = new Date().toISOString(); else updateData.completed_at = null; } if (priority !== undefined) updateData.priority = priority; if (estimated_hours !== undefined) updateData.estimated_hours = estimated_hours; if (actual_hours !== undefined) updateData.actual_hours = actual_hours; if (assigned_to !== undefined) updateData.assigned_to = assigned_to; if (due_date !== undefined) updateData.due_date = due_date; const { data, error } = await supabase .from("gameforge_tasks") .update(updateData) .eq("id", id) .select() .single(); if (error) throw error; res.json(data); } catch (err: any) { console.error("[GameForge Tasks] Update error:", err); res.status(500).json({ error: err.message }); } }); // ==================== METRICS ROUTES ==================== /** * GET /api/gameforge/metrics - Get project metrics */ router.get("/metrics", requireAuth, async (req: Request, res: Response) => { try { const { project_id, metric_type, limit = 50, offset = 0 } = req.query; if (!project_id) { return res.status(400).json({ error: "project_id is required" }); } let query = supabase .from("gameforge_metrics") .select("*", { count: "exact" }) .eq("project_id", project_id); if (metric_type) query = query.eq("metric_type", metric_type as string); const { data, error, count } = await query .order("metric_date", { ascending: false }) .range(Number(offset), Number(offset) + Number(limit) - 1); if (error) throw error; res.json({ metrics: data || [], total: count || 0, limit: Number(limit), offset: Number(offset) }); } catch (err: any) { console.error("[GameForge Metrics] List error:", err); res.status(500).json({ error: err.message }); } }); /** * POST /api/gameforge/metrics - Record project metrics */ router.post("/metrics", requireAuth, async (req: Request, res: Response) => { try { const userId = getUserId(req); if (!userId) return res.status(401).json({ error: "Unauthorized" }); const { project_id, metric_type, metric_date, velocity, hours_logged, team_size_avg, bugs_found, bugs_fixed, build_count, days_from_planned_to_release, on_schedule, budget_allocated, budget_spent } = req.body; if (!project_id || !metric_type) { return res.status(400).json({ error: "project_id and metric_type are required" }); } // Verify user is project lead const { data: project } = await supabase .from("gameforge_projects") .select("lead_id") .eq("id", project_id) .single(); if (project?.lead_id !== userId) { return res.status(403).json({ error: "Only project lead can record metrics" }); } const { data, error } = await supabase .from("gameforge_metrics") .insert({ project_id, metric_type, metric_date: metric_date || new Date().toISOString(), velocity, hours_logged, team_size_avg, bugs_found, bugs_fixed, build_count, days_from_planned_to_release, on_schedule, budget_allocated, budget_spent }) .select() .single(); if (error) throw error; res.status(201).json(data); } catch (err: any) { console.error("[GameForge Metrics] Create error:", err); res.status(500).json({ error: err.message }); } }); export default router;