// controllers/api/v1/project.ts // Copyright (C) 2026 Robert Colbert // All Rights Reserved import { Request, Response } from "express"; import { DtpController } from "../../../lib/controller.js"; import { IUser, ProjectStatus } from "@gadget/api"; import projectService from "../../../services/project.js"; export class ProjectApiControllerV1 extends DtpController { get name(): string { return "ProjectApiControllerV1"; } get slug(): string { return "projectV1"; } get route(): string { return "/projects"; } constructor() { super(); } async start(): Promise { this.router.use(this.requireUser()); this.router.get("/", this.getProjects.bind(this)); this.router.post("/", this.createProject.bind(this)); this.router.post("/pull", this.pullProject.bind(this)); this.router.get("/:projectId", this.getProject.bind(this)); this.router.put("/:projectId", this.updateProject.bind(this)); this.router.delete("/:projectId", this.deleteProject.bind(this)); } async getProjects(req: Request, res: Response): Promise { try { const projects = await projectService.getForUser(req.user); res.status(200).json({ success: true, data: projects, }); } catch (error) { this.log.error("failed to get projects", { error }); res.status(500).json({ success: false, message: "failed to get projects", }); } } async createProject(req: Request, res: Response): Promise { try { const { name, slug, gitUrl, status } = req.body; if (!name || !slug) { res.status(400).json({ success: false, message: "name and slug are required", }); return; } const project = await projectService.create(req.user, { name, slug, gitUrl, status: status as ProjectStatus, }); res.status(201).json({ success: true, data: project, }); } catch (error) { this.log.error("failed to create project", { error }); res.status(500).json({ success: false, message: "failed to create project", }); } } async pullProject(req: Request, res: Response): Promise { try { const { gitUrl } = req.body; if (!gitUrl || typeof gitUrl !== "string") { res.status(400).json({ success: false, message: "gitUrl is required", }); return; } // Validate the URL is parseable const trimmedUrl = gitUrl.trim(); const isSshFormat = trimmedUrl.includes(":") && !trimmedUrl.startsWith("http") && !trimmedUrl.startsWith("ssh://"); if (!isSshFormat) { try { new URL(trimmedUrl); } catch { res.status(400).json({ success: false, message: "invalid git URL format", }); return; } } const project = await projectService.createFromGitUrl(req.user, trimmedUrl); res.status(201).json({ success: true, data: project, }); } catch (error: any) { if (error.statusCode === 400) { res.status(400).json({ success: false, message: error.message, }); return; } // Handle duplicate gitUrl (unique partial index violation) if (error.code === 11000) { res.status(409).json({ success: false, message: "a project with this git URL already exists", }); return; } this.log.error("failed to pull project", { error }); res.status(500).json({ success: false, message: "failed to pull project", }); } } async getProject(req: Request, res: Response): Promise { try { const id = req.params.projectId as string; const project = await projectService.findById(id); if (!project) { res.status(404).json({ success: false, message: "project not found", }); return; } const user = project.user as IUser; if (user._id !== req.user._id) { res.status(403).json({ success: false, message: "access denied", }); return; } res.status(200).json({ success: true, data: project, }); } catch (error) { this.log.error("failed to get project", { error }); res.status(500).json({ success: false, message: "failed to get project", }); } } async updateProject(req: Request, res: Response): Promise { try { const id = req.params.projectId as string; const project = await projectService.findById(id); if (!project) { res.status(404).json({ success: false, message: "project not found", }); return; } const user = project.user as IUser; if (user._id !== req.user._id) { res.status(403).json({ success: false, message: "access denied", }); return; } const { name, slug, gitUrl, status, description, system, skills, tasks } = req.body; const updated = await projectService.update(project, { name, slug, gitUrl, status: status as ProjectStatus, description, system, skills, tasks, }); res.status(200).json({ success: true, data: updated, }); } catch (error) { this.log.error("failed to update project", { error }); res.status(500).json({ success: false, message: "failed to update project", }); } } async deleteProject(req: Request, res: Response): Promise { try { const id = req.params.projectId as string; const project = await projectService.findById(id); if (!project) { res.status(404).json({ success: false, message: "project not found", }); return; } const user = project.user as IUser; if (user._id !== req.user._id) { res.status(403).json({ success: false, message: "access denied", }); return; } await projectService.delete(project); res.status(200).json({ success: true, message: "project deleted", }); } catch (error) { this.log.error("failed to delete project", { error }); res.status(500).json({ success: false, message: "failed to delete project", }); } } } export default ProjectApiControllerV1;