Merge branch 'development'

This commit is contained in:
2026-01-29 15:22:47 +00:00
23 changed files with 455 additions and 749 deletions

View File

@@ -18,5 +18,4 @@ STRIPE_SECRET_KEY=your_stripe_secret_key
RESEND_API_KEY=re_xxxxxxxxxxxxxxxx
EMAIL_FROM=Sprint <support@sprintpm.org>
# password for demo accounts in seed data
SEED_PASSWORD=change_me_in_production
SEED_PASSWORD=replace-in-production

View File

@@ -2,11 +2,11 @@ import { IssueCreateRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import {
createIssue,
FREE_TIER_LIMITS,
getOrganisationIssueCount,
// FREE_TIER_LIMITS,
// getOrganisationIssueCount,
getOrganisationMemberRole,
getProjectByID,
getUserById,
// getUserById,
} from "../../db/queries";
import { errorResponse, parseJsonBody } from "../../validation";
@@ -34,17 +34,17 @@ export default async function issueCreate(req: AuthedRequest) {
}
// check free tier limit
const user = await getUserById(req.userId);
if (user && user.plan !== "pro") {
const issueCount = await getOrganisationIssueCount(project.organisationId);
if (issueCount >= FREE_TIER_LIMITS.issuesPerOrganisation) {
return errorResponse(
`free tier is limited to ${FREE_TIER_LIMITS.issuesPerOrganisation} issues per organisation. upgrade to pro for unlimited issues.`,
"FREE_TIER_ISSUE_LIMIT",
403,
);
}
}
// const user = await getUserById(req.userId);
// if (user && user.plan !== "pro") {
// const issueCount = await getOrganisationIssueCount(project.organisationId);
// if (issueCount >= FREE_TIER_LIMITS.issuesPerOrganisation) {
// return errorResponse(
// `free tier is limited to ${FREE_TIER_LIMITS.issuesPerOrganisation} issues per organisation. upgrade to pro for unlimited issues.`,
// "FREE_TIER_ISSUE_LIMIT",
// 403,
// );
// }
// }
const issue = await createIssue(
project.id,

View File

@@ -2,10 +2,10 @@ import { OrgAddMemberRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import {
createOrganisationMember,
FREE_TIER_LIMITS,
// FREE_TIER_LIMITS,
getOrganisationById,
getOrganisationMemberRole,
getOrganisationMembers,
// getOrganisationMembers,
getUserById,
} from "../../db/queries";
import { updateSeatCount } from "../../lib/seats";
@@ -42,17 +42,17 @@ export default async function organisationAddMember(req: AuthedRequest) {
}
// check free tier member limit
const requester = await getUserById(req.userId);
if (requester && requester.plan !== "pro") {
const members = await getOrganisationMembers(organisationId);
if (members.length >= FREE_TIER_LIMITS.membersPerOrganisation) {
return errorResponse(
`free tier is limited to ${FREE_TIER_LIMITS.membersPerOrganisation} members per organisation. upgrade to pro for unlimited members.`,
"FREE_TIER_MEMBER_LIMIT",
403,
);
}
}
// const requester = await getUserById(req.userId);
// if (requester && requester.plan !== "pro") {
// const members = await getOrganisationMembers(organisationId);
// if (members.length >= FREE_TIER_LIMITS.membersPerOrganisation) {
// return errorResponse(
// `free tier is limited to ${FREE_TIER_LIMITS.membersPerOrganisation} members per organisation. upgrade to pro for unlimited members.`,
// "FREE_TIER_MEMBER_LIMIT",
// 403,
// );
// }
// }
const member = await createOrganisationMember(organisationId, userId, role);

View File

@@ -2,10 +2,10 @@ import { OrgCreateRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import {
createOrganisationWithOwner,
FREE_TIER_LIMITS,
// FREE_TIER_LIMITS,
getOrganisationBySlug,
getUserById,
getUserOrganisationCount,
// getUserById,
// getUserOrganisationCount,
} from "../../db/queries";
import { errorResponse, parseJsonBody } from "../../validation";
@@ -21,17 +21,17 @@ export default async function organisationCreate(req: AuthedRequest) {
}
// check free tier limit
const user = await getUserById(req.userId);
if (user && user.plan !== "pro") {
const orgCount = await getUserOrganisationCount(req.userId);
if (orgCount >= FREE_TIER_LIMITS.organisationsPerUser) {
return errorResponse(
`free tier is limited to ${FREE_TIER_LIMITS.organisationsPerUser} organisation. upgrade to pro for unlimited organisations.`,
"FREE_TIER_ORG_LIMIT",
403,
);
}
}
// const user = await getUserById(req.userId);
// if (user && user.plan !== "pro") {
// const orgCount = await getUserOrganisationCount(req.userId);
// if (orgCount >= FREE_TIER_LIMITS.organisationsPerUser) {
// return errorResponse(
// `free tier is limited to ${FREE_TIER_LIMITS.organisationsPerUser} organisation. upgrade to pro for unlimited organisations.`,
// "FREE_TIER_ORG_LIMIT",
// 403,
// );
// }
// }
const organisation = await createOrganisationWithOwner(name, slug, req.userId, description);

View File

@@ -5,8 +5,8 @@ import {
getOrganisationById,
getOrganisationMemberRole,
getOrganisationMemberTimedSessions,
getOrganisationOwner,
getUserById,
// getOrganisationOwner,
// getUserById,
} from "../../db/queries";
import { errorResponse, parseQueryParams } from "../../validation";
@@ -40,9 +40,9 @@ export default async function organisationMemberTimeTracking(req: AuthedRequest)
}
// check if organisation owner has pro subscription
const owner = await getOrganisationOwner(organisationId);
const ownerUser = owner ? await getUserById(owner.userId) : null;
const isPro = ownerUser?.plan === "pro";
// const owner = await getOrganisationOwner(organisationId);
// const ownerUser = owner ? await getUserById(owner.userId) : null;
// const isPro = ownerUser?.plan === "pro";
const sessions = await getOrganisationMemberTimedSessions(organisationId, fromDate);
@@ -57,12 +57,12 @@ export default async function organisationMemberTimeTracking(req: AuthedRequest)
issueId: session.issueId,
issueNumber: session.issueNumber,
projectKey: session.projectKey,
timestamps: isPro ? session.timestamps : [],
endedAt: isPro ? session.endedAt : null,
createdAt: isPro ? session.createdAt : null,
workTimeMs: isPro ? actualWorkTimeMs : 0,
breakTimeMs: isPro ? actualBreakTimeMs : 0,
isRunning: isPro ? session.endedAt === null && isTimerRunning(timestamps) : false,
timestamps: session.timestamps,
endedAt: session.endedAt,
createdAt: session.createdAt,
workTimeMs: actualWorkTimeMs,
breakTimeMs: actualBreakTimeMs,
isRunning: session.endedAt === null && isTimerRunning(timestamps),
};
});

View File

@@ -2,9 +2,9 @@ import { ProjectCreateRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import {
createProject,
FREE_TIER_LIMITS,
// FREE_TIER_LIMITS,
getOrganisationMemberRole,
getOrganisationProjectCount,
// getOrganisationProjectCount,
getProjectByKey,
getUserById,
} from "../../db/queries";
@@ -30,18 +30,19 @@ export default async function projectCreate(req: AuthedRequest) {
}
// check free tier limit
const creator = await getUserById(req.userId);
if (creator && creator.plan !== "pro") {
const projectCount = await getOrganisationProjectCount(organisationId);
if (projectCount >= FREE_TIER_LIMITS.projectsPerOrganisation) {
return errorResponse(
`free tier is limited to ${FREE_TIER_LIMITS.projectsPerOrganisation} project per organisation. upgrade to pro for unlimited projects.`,
"FREE_TIER_PROJECT_LIMIT",
403,
);
}
}
// const creator = await getUserById(req.userId);
// if (creator && creator.plan !== "pro") {
// const projectCount = await getOrganisationProjectCount(organisationId);
// if (projectCount >= FREE_TIER_LIMITS.projectsPerOrganisation) {
// return errorResponse(
// `free tier is limited to ${FREE_TIER_LIMITS.projectsPerOrganisation} project per organisation. upgrade to pro for unlimited projects.`,
// "FREE_TIER_PROJECT_LIMIT",
// 403,
// );
// }
// }
const creator = await getUserById(req.userId);
if (!creator) {
return errorResponse(`creator not found`, "CREATOR_NOT_FOUND", 404);
}

View File

@@ -2,11 +2,11 @@ import { SprintCreateRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import {
createSprint,
FREE_TIER_LIMITS,
// FREE_TIER_LIMITS,
getOrganisationMemberRole,
getProjectByID,
getProjectSprintCount,
getSubscriptionByUserId,
// getProjectSprintCount,
// getSubscriptionByUserId,
hasOverlappingSprints,
} from "../../db/queries";
import { errorResponse, parseJsonBody } from "../../validation";
@@ -32,18 +32,18 @@ export default async function sprintCreate(req: AuthedRequest) {
}
// check free tier sprint limit
const subscription = await getSubscriptionByUserId(req.userId);
const isPro = subscription?.status === "active";
if (!isPro) {
const sprintCount = await getProjectSprintCount(projectId);
if (sprintCount >= FREE_TIER_LIMITS.sprintsPerProject) {
return errorResponse(
`Free tier limited to ${FREE_TIER_LIMITS.sprintsPerProject} sprints per project. Upgrade to Pro for unlimited sprints.`,
"SPRINT_LIMIT_REACHED",
403,
);
}
}
// const subscription = await getSubscriptionByUserId(req.userId);
// const isPro = subscription?.status === "active";
// if (!isPro) {
// const sprintCount = await getProjectSprintCount(projectId);
// if (sprintCount >= FREE_TIER_LIMITS.sprintsPerProject) {
// return errorResponse(
// `Free tier limited to ${FREE_TIER_LIMITS.sprintsPerProject} sprints per project. Upgrade to Pro for unlimited sprints.`,
// "SPRINT_LIMIT_REACHED",
// 403,
// );
// }
// }
const start = new Date(startDate);
const end = new Date(endDate);

View File

@@ -1,7 +1,7 @@
import { UserUpdateRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import { hashPassword } from "../../auth/utils";
import { getSubscriptionByUserId, getUserById } from "../../db/queries";
import { getUserById } from "../../db/queries";
import { errorResponse, parseJsonBody } from "../../validation";
export default async function update(req: AuthedRequest) {
@@ -24,17 +24,17 @@ export default async function update(req: AuthedRequest) {
}
// block free users from changing icon preference
if (iconPreference !== undefined && iconPreference !== user.iconPreference) {
const subscription = await getSubscriptionByUserId(req.userId);
const isPro = subscription?.status === "active";
if (!isPro) {
return errorResponse(
"icon style customization is only available on Pro. Upgrade to customize your icon style.",
"ICON_STYLE_PRO_ONLY",
403,
);
}
}
// if (iconPreference !== undefined && iconPreference !== user.iconPreference) {
// const subscription = await getSubscriptionByUserId(req.userId);
// const isPro = subscription?.status === "active";
// if (!isPro) {
// return errorResponse(
// "icon style customization is only available on Pro. Upgrade to customize your icon style.",
// "ICON_STYLE_PRO_ONLY",
// 403,
// );
// }
// }
let passwordHash: string | undefined;
if (password !== undefined) {

View File

@@ -1,7 +1,7 @@
import { randomUUID } from "node:crypto";
import sharp from "sharp";
import type { AuthedRequest } from "../../auth/middleware";
import { getSubscriptionByUserId } from "../../db/queries";
// import { getSubscriptionByUserId } from "../../db/queries";
import { s3Client, s3Endpoint, s3PublicUrl } from "../../s3";
const MAX_FILE_SIZE = 5 * 1024 * 1024;
@@ -42,21 +42,21 @@ export default async function uploadAvatar(req: AuthedRequest) {
const inputBuffer = Buffer.from(await file.arrayBuffer());
// check if user is pro
const subscription = await getSubscriptionByUserId(req.userId);
const isPro = subscription?.status === "active";
// const subscription = await getSubscriptionByUserId(req.userId);
// const isPro = subscription?.status === "active";
// block animated avatars for free users
if (!isPro && file.type === "image/gif") {
const animated = await isAnimatedGIF(inputBuffer);
if (animated) {
return new Response(
JSON.stringify({
error: "Animated avatars are only available on Pro. Upgrade to upload animated avatars.",
}),
{ status: 403, headers: { "Content-Type": "application/json" } },
);
}
}
// if (!isPro && file.type === "image/gif") {
// const animated = await isAnimatedGIF(inputBuffer);
// if (animated) {
// return new Response(
// JSON.stringify({
// error: "Animated avatars are only available on Pro. Upgrade to upload animated avatars.",
// }),
// { status: 403, headers: { "Content-Type": "application/json" } },
// );
// }
// }
const isGIF = file.type === "image/gif";
const outputExtension = isGIF ? "gif" : "png";