From 3cef3d3827b00f0cd72fb8040534846634e01e57 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 12 Jan 2026 01:05:53 +0000 Subject: [PATCH] sprint routes --- packages/backend/src/index.ts | 3 + packages/backend/src/routes/index.ts | 5 ++ packages/backend/src/routes/sprint/create.ts | 63 +++++++++++++++++++ .../backend/src/routes/sprints/by-project.ts | 31 +++++++++ 4 files changed, 102 insertions(+) create mode 100644 packages/backend/src/routes/sprint/create.ts create mode 100644 packages/backend/src/routes/sprints/by-project.ts diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 1eb588c..0fc8694 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -69,6 +69,9 @@ const main = async () => { "/projects/all": withCors(withAuth(routes.projectsAll)), "/projects/with-creators": withCors(withAuth(routes.projectsWithCreators)), + "/sprint/create": withCors(withAuth(withCSRF(routes.sprintCreate))), + "/sprints/by-project": withCors(withAuth(routes.sprintsByProject)), + "/timer/toggle": withCors(withAuth(withCSRF(routes.timerToggle))), "/timer/end": withCors(withAuth(withCSRF(routes.timerEnd))), "/timer/get": withCors(withAuth(withCSRF(routes.timerGet))), diff --git a/packages/backend/src/routes/index.ts b/packages/backend/src/routes/index.ts index 593b3e6..9d4e4ff 100644 --- a/packages/backend/src/routes/index.ts +++ b/packages/backend/src/routes/index.ts @@ -26,6 +26,8 @@ import projectDelete from "./project/delete"; import projectUpdate from "./project/update"; import projectWithCreator from "./project/with-creator"; import projectsWithCreators from "./project/with-creators"; +import sprintCreate from "./sprint/create"; +import sprintsByProject from "./sprints/by-project"; import timerEnd from "./timer/end"; import timerGet from "./timer/get"; import timerGetInactive from "./timer/get-inactive"; @@ -75,6 +77,9 @@ export const routes = { projectsAll, projectsWithCreators, + sprintCreate, + sprintsByProject, + timerToggle, timerGet, timerGetInactive, diff --git a/packages/backend/src/routes/sprint/create.ts b/packages/backend/src/routes/sprint/create.ts new file mode 100644 index 0000000..566c160 --- /dev/null +++ b/packages/backend/src/routes/sprint/create.ts @@ -0,0 +1,63 @@ +import type { AuthedRequest } from "../../auth/middleware"; +import { createSprint, getOrganisationMemberRole, getProjectByID } from "../../db/queries"; + +// /sprint/create?projectId=1&name=Sprint%201&startDate=2025-01-01T00:00:00.000Z&endDate=2025-01-14T23:59:00.000Z +export default async function sprintCreate(req: AuthedRequest) { + const url = new URL(req.url); + const projectId = url.searchParams.get("projectId"); + const name = url.searchParams.get("name"); + const color = url.searchParams.get("color") || undefined; + const startDateParam = url.searchParams.get("startDate"); + const endDateParam = url.searchParams.get("endDate"); + + if (!projectId || !name || !startDateParam || !endDateParam) { + return new Response( + `missing parameters: ${!projectId ? "projectId " : ""}${!name ? "name " : ""}${ + !startDateParam ? "startDate " : "" + }${!endDateParam ? "endDate" : ""}`, + { status: 400 }, + ); + } + + const projectIdNumber = Number(projectId); + if (!Number.isInteger(projectIdNumber)) { + return new Response("projectId must be an integer", { status: 400 }); + } + + const project = await getProjectByID(projectIdNumber); + if (!project) { + return new Response(`project not found: provided ${projectId}`, { status: 404 }); + } + + const membership = await getOrganisationMemberRole(project.organisationId, req.userId); + if (!membership) { + return new Response("not a member of this organisation", { status: 403 }); + } + + if (membership.role !== "owner" && membership.role !== "admin") { + return new Response("only owners and admins can create sprints", { status: 403 }); + } + + const trimmedName = name.trim(); + if (trimmedName === "") { + return new Response("name cannot be empty", { status: 400 }); + } + + const startDate = new Date(startDateParam); + if (Number.isNaN(startDate.valueOf())) { + return new Response("startDate must be a valid date", { status: 400 }); + } + + const endDate = new Date(endDateParam); + if (Number.isNaN(endDate.valueOf())) { + return new Response("endDate must be a valid date", { status: 400 }); + } + + if (startDate > endDate) { + return new Response("endDate must be after startDate", { status: 400 }); + } + + const sprint = await createSprint(project.id, trimmedName, color, startDate, endDate); + + return Response.json(sprint); +} diff --git a/packages/backend/src/routes/sprints/by-project.ts b/packages/backend/src/routes/sprints/by-project.ts new file mode 100644 index 0000000..d929d48 --- /dev/null +++ b/packages/backend/src/routes/sprints/by-project.ts @@ -0,0 +1,31 @@ +import type { AuthedRequest } from "../../auth/middleware"; +import { getOrganisationMemberRole, getProjectByID, getSprintsByProject } from "../../db/queries"; + +// /sprints/by-project?projectId=1 +export default async function sprintsByProject(req: AuthedRequest) { + const url = new URL(req.url); + const projectId = url.searchParams.get("projectId"); + + if (!projectId) { + return new Response("missing projectId", { status: 400 }); + } + + const projectIdNumber = Number(projectId); + if (!Number.isInteger(projectIdNumber)) { + return new Response("projectId must be an integer", { status: 400 }); + } + + const project = await getProjectByID(projectIdNumber); + if (!project) { + return new Response(`project not found: provided ${projectId}`, { status: 404 }); + } + + const membership = await getOrganisationMemberRole(project.organisationId, req.userId); + if (!membership) { + return new Response("not a member of this organisation", { status: 403 }); + } + + const sprints = await getSprintsByProject(project.id); + + return Response.json(sprints); +}