262 lines
6.5 KiB
TypeScript
262 lines
6.5 KiB
TypeScript
// controllers/api/v1/project.ts
|
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
|
// 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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;
|