mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 10:33:01 +00:00
edit + delete capabilities for org, project, sprint
This commit is contained in:
@@ -27,6 +27,8 @@ import projectUpdate from "./project/update";
|
||||
import projectWithCreator from "./project/with-creator";
|
||||
import projectsWithCreators from "./project/with-creators";
|
||||
import sprintCreate from "./sprint/create";
|
||||
import sprintDelete from "./sprint/delete";
|
||||
import sprintUpdate from "./sprint/update";
|
||||
import sprintsByProject from "./sprints/by-project";
|
||||
import timerEnd from "./timer/end";
|
||||
import timerGet from "./timer/get";
|
||||
@@ -78,6 +80,8 @@ export const routes = {
|
||||
projectsWithCreators,
|
||||
|
||||
sprintCreate,
|
||||
sprintUpdate,
|
||||
sprintDelete,
|
||||
sprintsByProject,
|
||||
|
||||
timerToggle,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { OrgDeleteRequestSchema } from "@sprint/shared";
|
||||
import type { BunRequest } from "bun";
|
||||
import { deleteOrganisation, getOrganisationById } from "../../db/queries";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { deleteOrganisation, getOrganisationById, getOrganisationMemberRole } from "../../db/queries";
|
||||
import { errorResponse, parseJsonBody } from "../../validation";
|
||||
|
||||
export default async function organisationDelete(req: BunRequest) {
|
||||
export default async function organisationDelete(req: AuthedRequest) {
|
||||
const parsed = await parseJsonBody(req, OrgDeleteRequestSchema);
|
||||
if ("error" in parsed) return parsed.error;
|
||||
|
||||
@@ -14,6 +14,14 @@ export default async function organisationDelete(req: BunRequest) {
|
||||
return errorResponse(`organisation with id ${id} not found`, "ORG_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
const requesterMember = await getOrganisationMemberRole(id, req.userId);
|
||||
if (!requesterMember) {
|
||||
return errorResponse("you are not a member of this organisation", "NOT_MEMBER", 403);
|
||||
}
|
||||
if (requesterMember.role !== "owner") {
|
||||
return errorResponse("only owners can delete organisations", "PERMISSION_DENIED", 403);
|
||||
}
|
||||
|
||||
await deleteOrganisation(id);
|
||||
|
||||
return Response.json({ success: true });
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { OrgUpdateRequestSchema } from "@sprint/shared";
|
||||
import type { BunRequest } from "bun";
|
||||
import { getOrganisationById, updateOrganisation } from "../../db/queries";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { getOrganisationById, getOrganisationMemberRole, updateOrganisation } from "../../db/queries";
|
||||
import { errorResponse, parseJsonBody } from "../../validation";
|
||||
|
||||
export default async function organisationUpdate(req: BunRequest) {
|
||||
export default async function organisationUpdate(req: AuthedRequest) {
|
||||
const parsed = await parseJsonBody(req, OrgUpdateRequestSchema);
|
||||
if ("error" in parsed) return parsed.error;
|
||||
|
||||
@@ -14,6 +14,14 @@ export default async function organisationUpdate(req: BunRequest) {
|
||||
return errorResponse(`organisation with id ${id} does not exist`, "ORG_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
const requesterMember = await getOrganisationMemberRole(id, req.userId);
|
||||
if (!requesterMember) {
|
||||
return errorResponse("you are not a member of this organisation", "NOT_MEMBER", 403);
|
||||
}
|
||||
if (requesterMember.role !== "owner" && requesterMember.role !== "admin") {
|
||||
return errorResponse("only owners and admins can edit organisations", "PERMISSION_DENIED", 403);
|
||||
}
|
||||
|
||||
if (!name && !description && !slug && !statuses) {
|
||||
return errorResponse(
|
||||
"at least one of name, description, slug, or statuses must be provided",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ProjectDeleteRequestSchema } from "@sprint/shared";
|
||||
import type { BunRequest } from "bun";
|
||||
import { deleteProject, getProjectByID } from "../../db/queries";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { deleteProject, getOrganisationMemberRole, getProjectByID } from "../../db/queries";
|
||||
import { errorResponse, parseJsonBody } from "../../validation";
|
||||
|
||||
export default async function projectDelete(req: BunRequest) {
|
||||
export default async function projectDelete(req: AuthedRequest) {
|
||||
const parsed = await parseJsonBody(req, ProjectDeleteRequestSchema);
|
||||
if ("error" in parsed) return parsed.error;
|
||||
|
||||
@@ -14,6 +14,22 @@ export default async function projectDelete(req: BunRequest) {
|
||||
return errorResponse(`project with id ${id} does not exist`, "PROJECT_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
const requesterMember = await getOrganisationMemberRole(existingProject.organisationId, req.userId);
|
||||
if (!requesterMember) {
|
||||
return errorResponse("you are not a member of this organisation", "NOT_MEMBER", 403);
|
||||
}
|
||||
|
||||
const isOrgOwner = requesterMember.role === "owner";
|
||||
const isProjectCreator = existingProject.creatorId === req.userId;
|
||||
|
||||
if (!isOrgOwner && !isProjectCreator) {
|
||||
return errorResponse(
|
||||
"only organisation owners or the project creator can delete projects",
|
||||
"PERMISSION_DENIED",
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
await deleteProject(id);
|
||||
|
||||
return Response.json({ success: true });
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { ProjectUpdateRequestSchema } from "@sprint/shared";
|
||||
import type { BunRequest } from "bun";
|
||||
import { getProjectByID, getProjectByKey, getUserById, updateProject } from "../../db/queries";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import {
|
||||
getOrganisationMemberRole,
|
||||
getProjectByID,
|
||||
getProjectByKey,
|
||||
getUserById,
|
||||
updateProject,
|
||||
} from "../../db/queries";
|
||||
import { errorResponse, parseJsonBody } from "../../validation";
|
||||
|
||||
export default async function projectUpdate(req: BunRequest) {
|
||||
export default async function projectUpdate(req: AuthedRequest) {
|
||||
const parsed = await parseJsonBody(req, ProjectUpdateRequestSchema);
|
||||
if ("error" in parsed) return parsed.error;
|
||||
|
||||
@@ -14,6 +20,18 @@ export default async function projectUpdate(req: BunRequest) {
|
||||
return errorResponse(`project with id ${id} does not exist`, "PROJECT_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
const requesterMember = await getOrganisationMemberRole(existingProject.organisationId, req.userId);
|
||||
if (!requesterMember) {
|
||||
return errorResponse("you are not a member of this organisation", "NOT_MEMBER", 403);
|
||||
}
|
||||
if (requesterMember.role !== "owner" && requesterMember.role !== "admin") {
|
||||
return errorResponse(
|
||||
"only organisation owners and admins can edit projects",
|
||||
"PERMISSION_DENIED",
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
if (!key && !name && !creatorId && !organisationId) {
|
||||
return errorResponse(
|
||||
"at least one of key, name, creatorId, or organisationId must be provided",
|
||||
|
||||
42
packages/backend/src/routes/sprint/delete.ts
Normal file
42
packages/backend/src/routes/sprint/delete.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { SprintDeleteRequestSchema } from "@sprint/shared";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { deleteSprint, getOrganisationMemberRole, getProjectByID, getSprintById } from "../../db/queries";
|
||||
import { errorResponse, parseJsonBody } from "../../validation";
|
||||
|
||||
export default async function sprintDelete(req: AuthedRequest) {
|
||||
const parsed = await parseJsonBody(req, SprintDeleteRequestSchema);
|
||||
if ("error" in parsed) return parsed.error;
|
||||
|
||||
const { id } = parsed.data;
|
||||
|
||||
const existingSprint = await getSprintById(id);
|
||||
if (!existingSprint) {
|
||||
return errorResponse(`sprint with id ${id} does not exist`, "SPRINT_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
const project = await getProjectByID(existingSprint.projectId);
|
||||
if (!project) {
|
||||
return errorResponse("project not found", "PROJECT_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
const requesterMember = await getOrganisationMemberRole(project.organisationId, req.userId);
|
||||
if (!requesterMember) {
|
||||
return errorResponse("you are not a member of this organisation", "NOT_MEMBER", 403);
|
||||
}
|
||||
|
||||
const isOrgOwner = requesterMember.role === "owner";
|
||||
const isAdmin = requesterMember.role === "admin";
|
||||
const isProjectCreator = project.creatorId === req.userId;
|
||||
|
||||
if (!isOrgOwner && !isAdmin && !isProjectCreator) {
|
||||
return errorResponse(
|
||||
"only organisation owners, admins, or project creators can delete sprints",
|
||||
"PERMISSION_DENIED",
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
await deleteSprint(id);
|
||||
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
76
packages/backend/src/routes/sprint/update.ts
Normal file
76
packages/backend/src/routes/sprint/update.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { SprintUpdateRequestSchema } from "@sprint/shared";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import {
|
||||
getOrganisationMemberRole,
|
||||
getProjectByID,
|
||||
getSprintById,
|
||||
hasOverlappingSprints,
|
||||
updateSprint,
|
||||
} from "../../db/queries";
|
||||
import { errorResponse, parseJsonBody } from "../../validation";
|
||||
|
||||
export default async function sprintUpdate(req: AuthedRequest) {
|
||||
const parsed = await parseJsonBody(req, SprintUpdateRequestSchema);
|
||||
if ("error" in parsed) return parsed.error;
|
||||
|
||||
const { id, name, color, startDate, endDate } = parsed.data;
|
||||
|
||||
const existingSprint = await getSprintById(id);
|
||||
if (!existingSprint) {
|
||||
return errorResponse(`sprint with id ${id} does not exist`, "SPRINT_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
const project = await getProjectByID(existingSprint.projectId);
|
||||
if (!project) {
|
||||
return errorResponse("project not found", "PROJECT_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
const requesterMember = await getOrganisationMemberRole(project.organisationId, req.userId);
|
||||
if (!requesterMember) {
|
||||
return errorResponse("you are not a member of this organisation", "NOT_MEMBER", 403);
|
||||
}
|
||||
if (requesterMember.role !== "owner" && requesterMember.role !== "admin") {
|
||||
return errorResponse(
|
||||
"only organisation owners and admins can edit sprints",
|
||||
"PERMISSION_DENIED",
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
if (!name && !color && !startDate && !endDate) {
|
||||
return errorResponse(
|
||||
"at least one of name, color, startDate, or endDate must be provided",
|
||||
"NO_UPDATES",
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
// validate dates if provided
|
||||
const newStartDate = startDate ? new Date(startDate) : existingSprint.startDate;
|
||||
const newEndDate = endDate ? new Date(endDate) : existingSprint.endDate;
|
||||
|
||||
if (newStartDate > newEndDate) {
|
||||
return errorResponse("End date must be after start date", "INVALID_DATES", 400);
|
||||
}
|
||||
|
||||
if (startDate || endDate) {
|
||||
const hasOverlap = await hasOverlappingSprints(
|
||||
project.id,
|
||||
newStartDate,
|
||||
newEndDate,
|
||||
existingSprint.id,
|
||||
);
|
||||
if (hasOverlap) {
|
||||
return errorResponse("Sprint dates overlap with an existing sprint", "SPRINT_OVERLAP", 400);
|
||||
}
|
||||
}
|
||||
|
||||
const sprint = await updateSprint(id, {
|
||||
name,
|
||||
color,
|
||||
startDate: startDate ? newStartDate : undefined,
|
||||
endDate: endDate ? newEndDate : undefined,
|
||||
});
|
||||
|
||||
return Response.json(sprint);
|
||||
}
|
||||
Reference in New Issue
Block a user