Skip to content

Commit 23ae7ba

Browse files
committed
fix: apply review suggestions
1 parent 82eaa33 commit 23ae7ba

File tree

20 files changed

+258
-84
lines changed

20 files changed

+258
-84
lines changed

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"helmet": "^7.2.0",
3636
"jsonwebtoken": "^9.0.2",
3737
"razorpay": "^2.9.6",
38+
"superjson": "^2.2.5",
3839
"zeptomail": "^6.2.1",
3940
"zod": "^4.1.9"
4041
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Subscription status constants matching Prisma SubscriptionStatus enum
3+
* These are readonly string literals for type safety
4+
*/
5+
export const SUBSCRIPTION_STATUS = {
6+
CREATED: "created",
7+
AUTHENTICATED: "authenticated",
8+
ACTIVE: "active",
9+
PENDING: "pending",
10+
HALTED: "halted",
11+
CANCELLED: "cancelled",
12+
COMPLETED: "completed",
13+
EXPIRED: "expired",
14+
} as const;
15+
16+
/**
17+
* Payment status constants matching Prisma PaymentStatus enum
18+
* These are readonly string literals for type safety
19+
*/
20+
export const PAYMENT_STATUS = {
21+
CREATED: "created",
22+
AUTHORIZED: "authorized",
23+
CAPTURED: "captured",
24+
REFUNDED: "refunded",
25+
FAILED: "failed",
26+
} as const;
27+
28+
/**
29+
* Type for subscription status values
30+
*/
31+
export type SubscriptionStatusValue =
32+
(typeof SUBSCRIPTION_STATUS)[keyof typeof SUBSCRIPTION_STATUS];
33+
34+
/**
35+
* Type for payment status values
36+
*/
37+
export type PaymentStatusValue =
38+
(typeof PAYMENT_STATUS)[keyof typeof PAYMENT_STATUS];

apps/api/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import ipBlocker from "./middleware/ipBlock.js";
1313
import crypto from "crypto";
1414
import { paymentService } from "./services/payment.service.js";
1515
import { verifyToken } from "./utils/auth.js";
16+
import { SUBSCRIPTION_STATUS } from "./constants/subscription.js";
1617

1718
dotenv.config();
1819

@@ -118,7 +119,7 @@ app.get("/join-community", apiLimiter, async (req: Request, res: Response) => {
118119
const subscription = await prismaModule.prisma.subscription.findFirst({
119120
where: {
120121
userId: user.id,
121-
status: "active",
122+
status: SUBSCRIPTION_STATUS.ACTIVE,
122123
endDate: {
123124
gte: new Date(),
124125
},

apps/api/src/routers/payment.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import prismaModule from "../prisma.js";
77

88
const { prisma } = prismaModule;
99

10+
const ALLOWED_NOTE_KEYS = ["plan", "user_email"] as const;
11+
const MAX_NOTE_VALUE_LENGTH = 255;
12+
1013
const createOrderSchema = z.object({
1114
planId: z.string().min(1, "Plan ID is required"),
1215
receipt: z.string().min(1, "Receipt is required"),
@@ -59,18 +62,28 @@ export const paymentRouter = router({
5962
});
6063
}
6164

62-
// Add user_id and plan_id to notes for webhook processing
63-
const notesWithUserId = {
64-
...(input.notes || {}),
65-
user_id: userId,
66-
plan_id: input.planId,
67-
};
65+
const sanitizedNotes: Record<string, string> = {};
66+
67+
if (input.notes) {
68+
for (const key of ALLOWED_NOTE_KEYS) {
69+
if (input.notes[key]) {
70+
const value = String(input.notes[key]).slice(
71+
0,
72+
MAX_NOTE_VALUE_LENGTH
73+
);
74+
sanitizedNotes[key] = value;
75+
}
76+
}
77+
}
78+
79+
sanitizedNotes.user_id = userId;
80+
sanitizedNotes.plan_id = input.planId;
6881

6982
const result = await paymentService.createOrder({
7083
amount: plan.price, // Use price from database
7184
currency: plan.currency,
7285
receipt: input.receipt,
73-
notes: notesWithUserId,
86+
notes: sanitizedNotes,
7487
});
7588

7689
// Check if it's an error response

apps/api/src/services/auth.service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ export const authService = {
3939
authMethod: authMethod || "google",
4040
lastLogin: new Date(),
4141
},
42+
select: {
43+
id: true,
44+
email: true,
45+
firstName: true,
46+
authMethod: true,
47+
createdAt: true,
48+
lastLogin: true,
49+
},
4250
});
4351

4452
const token = generateToken(email);

apps/api/src/services/email.service.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ interface SendEmailInput {
1212
textBody?: string;
1313
}
1414

15+
const escapeHtml = (s: string): string =>
16+
s
17+
.replace(/&/g, "&amp;")
18+
.replace(/</g, "&lt;")
19+
.replace(/>/g, "&gt;")
20+
.replace(/"/g, "&quot;")
21+
.replace(/'/g, "&#39;");
22+
1523
// Initialize ZeptoMail client
1624
const initializeEmailClient = () => {
1725
const url =
@@ -77,10 +85,11 @@ export const emailService = {
7785
email: string,
7886
firstName: string
7987
): Promise<boolean> {
88+
const safeFirstName = escapeHtml(firstName);
8089
const htmlBody = `
8190
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
8291
<p style="color: #333; line-height: 1.8; font-size: 16px;">
83-
Hi ${firstName},
92+
Hi ${safeFirstName},
8493
</p>
8594
8695
<p style="color: #333; line-height: 1.8; font-size: 16px;">
@@ -127,7 +136,7 @@ export const emailService = {
127136
</div>
128137
`;
129138

130-
const textBody = `Hi ${firstName},
139+
const textBody = `Hi ${safeFirstName},
131140
132141
I am Ajeet, founder of Opensox AI.
133142
@@ -147,7 +156,7 @@ Best,
147156
Ajeet from Opensox.ai`;
148157

149158
return this.sendEmail({
150-
to: [{ address: email, name: firstName }],
159+
to: [{ address: email, name: safeFirstName }],
151160
subject: "Congratulations! You are in.",
152161
htmlBody,
153162
textBody,

apps/api/src/services/payment.service.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { rz_instance } from "../clients/razorpay.js";
22
import crypto from "crypto";
33
import prismaModule from "../prisma.js";
4+
import {
5+
SUBSCRIPTION_STATUS,
6+
PAYMENT_STATUS,
7+
} from "../constants/subscription.js";
48

59
const { prisma } = prismaModule;
610

@@ -115,16 +119,18 @@ export const paymentService = {
115119
}
116120

117121
// Create the expected signature
118-
const generatedSignature = crypto
122+
const generatedSignatureHex = crypto
119123
.createHmac("sha256", keySecret)
120124
.update(`${orderId}|${paymentId}`)
121125
.digest("hex");
122126

127+
const a = Buffer.from(signature, "hex");
128+
const b = Buffer.from(generatedSignatureHex, "hex");
129+
130+
if (a.length !== b.length) return false;
131+
123132
// Compare signatures securely
124-
return crypto.timingSafeEqual(
125-
Buffer.from(signature),
126-
Buffer.from(generatedSignature)
127-
);
133+
return crypto.timingSafeEqual(a, b);
128134
} catch (error) {
129135
console.error("Signature verification error:", error);
130136
return false;
@@ -156,7 +162,7 @@ export const paymentService = {
156162
razorpayOrderId: paymentData.razorpayOrderId,
157163
amount: paymentData.amount, // Amount in paise (smallest currency unit)
158164
currency: paymentData.currency,
159-
status: "captured",
165+
status: PAYMENT_STATUS.CAPTURED,
160166
},
161167
});
162168

@@ -229,7 +235,7 @@ export const paymentService = {
229235
data: {
230236
userId,
231237
planId,
232-
status: "active",
238+
status: SUBSCRIPTION_STATUS.ACTIVE,
233239
startDate,
234240
endDate,
235241
autoRenew: true,

apps/api/src/services/query.service.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { PrismaClient } from "@prisma/client";
1+
import type { PrismaClient } from "@prisma/client";
2+
import type prismaModule from "../prisma.js";
3+
4+
type ExtendedPrismaClient = typeof prismaModule.prisma;
25

36
export const queryService = {
47
/**
58
* Get total count of queries
69
*/
7-
async getQueryCount(prisma: PrismaClient) {
10+
async getQueryCount(prisma: ExtendedPrismaClient | PrismaClient) {
811
const queryCount = await prisma.queryCount.findUnique({
912
where: { id: 1 },
1013
});
@@ -17,7 +20,9 @@ export const queryService = {
1720
/**
1821
* Increment the query count by 1
1922
*/
20-
async incrementQueryCount(prisma: PrismaClient): Promise<void> {
23+
async incrementQueryCount(
24+
prisma: ExtendedPrismaClient | PrismaClient
25+
): Promise<void> {
2126
try {
2227
const updatedCount = await prisma.queryCount.update({
2328
where: { id: 1 },

apps/api/src/services/user.service.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { PrismaClient } from "@prisma/client";
1+
import type { PrismaClient } from "@prisma/client";
2+
import type prismaModule from "../prisma.js";
3+
import { SUBSCRIPTION_STATUS } from "../constants/subscription.js";
4+
5+
type ExtendedPrismaClient = typeof prismaModule.prisma;
26

37
export const userService = {
48
/**
59
* Get total count of users
610
*/
7-
async getUserCount(prisma: PrismaClient) {
11+
async getUserCount(prisma: ExtendedPrismaClient | PrismaClient) {
812
const userCount = await prisma.user.count();
913

1014
return {
@@ -15,11 +19,14 @@ export const userService = {
1519
/**
1620
* Check if user has an active subscription
1721
*/
18-
async checkSubscriptionStatus(prisma: PrismaClient, userId: string) {
22+
async checkSubscriptionStatus(
23+
prisma: ExtendedPrismaClient | PrismaClient,
24+
userId: string
25+
) {
1926
const subscription = await prisma.subscription.findFirst({
2027
where: {
2128
userId,
22-
status: "active",
29+
status: SUBSCRIPTION_STATUS.ACTIVE,
2330
endDate: {
2431
gte: new Date(),
2532
},

apps/api/src/trpc.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { initTRPC, TRPCError } from "@trpc/server";
2+
import superjson from "superjson";
23
import type { Context } from "./context.js";
34
import { verifyToken } from "./utils/auth.js";
45

5-
const t = initTRPC.context<Context>().create();
6+
const t = initTRPC.context<Context>().create({
7+
transformer: superjson,
8+
});
69

710
const isAuthed = t.middleware(async ({ ctx, next }) => {
811
const authHeader = ctx.req.headers.authorization;

0 commit comments

Comments
 (0)