Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/api/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
import prisma from "./prisma.js";
import type { User } from "@prisma/client";
import type { UserWithSubscription } from "./utils/auth.js";

export async function createContext({
req,
Expand All @@ -10,7 +10,7 @@ export async function createContext({
res: CreateExpressContextOptions["res"];
db: typeof prisma;
ip?: string;
user?: User | null;
user?: UserWithSubscription | null;
}> {
const ip = req.ip || req.socket.remoteAddress || "unknown";

Expand Down
14 changes: 1 addition & 13 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,26 +111,14 @@ app.get("/join-community", apiLimiter, async (req: Request, res: Response) => {

const token = authHeader.substring(7);

// Verify token and get user
let user;
try {
user = await verifyToken(token);
} catch (error) {
return res.status(401).json({ error: "Unauthorized - Invalid token" });
}

// Check if user has an active subscription
const subscription = await prismaModule.prisma.subscription.findFirst({
where: {
userId: user.id,
status: SUBSCRIPTION_STATUS.ACTIVE,
endDate: {
gte: new Date(),
},
},
});

if (!subscription) {
if (!user.isPaidUser || !user.subscription) {
return res.status(403).json({
error: "Forbidden - Active subscription required to join community",
});
Expand Down
46 changes: 45 additions & 1 deletion apps/api/src/routers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,51 @@ export const authRouter = router({
}),
getSession: protectedProcedure.query(
async ({ ctx }: { ctx: { user: any } }) => {
return authService.getSession(ctx.user);
const userId = ctx.user.id;
const user = await ctx.db.prisma.user.findUnique({
where: { id: userId },
include: {
subscriptions: {
where: {
status: "active",
endDate: {
gte: new Date(),
},
},
orderBy: {
startDate: "desc",
},
take: 1,
include: {
plan: true,
},
},
},
});

if (!user) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}

const activeSubscription = user.subscriptions[0] || null;

return authService.getSession({
...user,
isPaidUser: !!activeSubscription,
subscription: activeSubscription
? {
id: activeSubscription.id,
status: activeSubscription.status,
startDate: activeSubscription.startDate,
endDate: activeSubscription.endDate,
planId: activeSubscription.planId,
planName: activeSubscription.plan?.name,
}
: null,
});
}
Comment on lines 69 to 116
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -nP '\bSUBSCRIPTION_STATUS\b' --type=ts -A2

Repository: apsinghdev/opensox

Length of output: 2348


🏁 Script executed:

# Check the full auth.ts file to see type definitions and context
fd auth.ts --type f | head -5

Repository: apsinghdev/opensox

Length of output: 117


🏁 Script executed:

# Search for subscription status constants or enums
rg -n 'status.*active' apps/api/src --type=ts -B2 -A2 | head -50

Repository: apsinghdev/opensox

Length of output: 1508


🏁 Script executed:

# Get the full context of the auth.ts file to see type definitions
head -70 apps/api/src/routers/auth.ts

Repository: apsinghdev/opensox

Length of output: 2453


🏁 Script executed:

# Check the SUBSCRIPTION_STATUS constant definition
cat -n apps/api/src/constants/subscription.ts

Repository: apsinghdev/opensox

Length of output: 1275


🏁 Script executed:

# Check if there's a ProtectedProcedure type or context type definition
rg -n 'protectedProcedure\|type.*Context' apps/api/src/routers/auth.ts -B2 -A2

Repository: apsinghdev/opensox

Length of output: 44


🏁 Script executed:

# Check how protectedProcedure is defined and what context type it has
cat -n apps/api/src/trpc.ts | head -100

Repository: apsinghdev/opensox

Length of output: 2138


🏁 Script executed:

# Check if there are type definitions for the user object anywhere
rg -n 'type.*User\|interface.*User' apps/api/src/routers/auth.ts -B2 -A5

Repository: apsinghdev/opensox

Length of output: 44


🏁 Script executed:

# Check if SUBSCRIPTION_STATUS is imported in auth.ts
head -20 apps/api/src/routers/auth.ts | grep -n import

Repository: apsinghdev/opensox

Length of output: 321


Use the SUBSCRIPTION_STATUS constant instead of hardcoded string and replace any type with proper typing.

The code should use the SUBSCRIPTION_STATUS.ACTIVE constant (defined in constants/subscription.ts) instead of the hardcoded string "active" at line 77, consistent with the rest of the codebase (utils/auth.ts, services/auth.service.ts, services/user.service.ts, services/payment.service.ts).

Also, replace ctx: { user: any } with the proper UserWithSubscription type available from utils/auth.js and already imported in trpc.ts. The any type violates the API coding guideline that requires avoiding any type.

Required changes:

  • Import SUBSCRIPTION_STATUS from constants/subscription.js
  • Import UserWithSubscription type from utils/auth.js
  • Change line 77 from status: "active" to status: SUBSCRIPTION_STATUS.ACTIVE
  • Change line 70 from { ctx: { user: any } } to { ctx: { user: UserWithSubscription } }
🤖 Prompt for AI Agents
In apps/api/src/routers/auth.ts around lines 69 to 116, replace the inline any
type and hardcoded subscription string: change the function signature param from
"{ ctx: { user: any } }" to "{ ctx: { user: UserWithSubscription } }", import
the UserWithSubscription type from utils/auth.js (or .ts), import
SUBSCRIPTION_STATUS from constants/subscription.js (or .ts), and replace the
subscription where clause status: "active" with status:
SUBSCRIPTION_STATUS.ACTIVE; ensure imports are added at the top of the file and
that TypeScript resolves the type import correctly.

),
generateJWT: publicProcedure
Expand Down
61 changes: 49 additions & 12 deletions apps/api/src/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { generateToken } from "../utils/auth.js";
import type { PrismaClient } from "@prisma/client";
import { SUBSCRIPTION_STATUS } from "../constants/subscription.js";

interface GoogleAuthInput {
email: string;
Expand Down Expand Up @@ -39,20 +40,49 @@ export const authService = {
authMethod: authMethod || "google",
lastLogin: new Date(),
},
select: {
id: true,
email: true,
firstName: true,
authMethod: true,
createdAt: true,
lastLogin: true,
include: {
subscriptions: {
where: {
status: SUBSCRIPTION_STATUS.ACTIVE,
endDate: {
gte: new Date(),
},
},
orderBy: {
startDate: "desc",
},
take: 1,
include: {
plan: true,
},
},
},
});

const activeSubscription = user.subscriptions[0] || null;
const token = generateToken(email);

return {
user,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
authMethod: user.authMethod,
createdAt: user.createdAt,
lastLogin: user.lastLogin,
completedSteps: user.completedSteps,
isPaidUser: !!activeSubscription,
subscription: activeSubscription
? {
id: activeSubscription.id,
status: activeSubscription.status,
startDate: activeSubscription.startDate,
endDate: activeSubscription.endDate,
planId: activeSubscription.planId,
planName: activeSubscription.plan?.name,
}
: null,
},
token,
};
},
Expand Down Expand Up @@ -197,12 +227,19 @@ export const authService = {
});
},

/**
* Get user session information
*/
getSession(user: any) {
return {
user,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
authMethod: user.authMethod,
createdAt: user.createdAt,
lastLogin: user.lastLogin,
completedSteps: user.completedSteps,
isPaidUser: user.isPaidUser || false,
subscription: user.subscription || null,
},
};
},
};
32 changes: 29 additions & 3 deletions apps/api/src/trpc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import type { Context } from "./context.js";
import { verifyToken } from "./utils/auth.js";
import { verifyToken, type UserWithSubscription } from "./utils/auth.js";

const t = initTRPC.context<Context>().create({
transformer: superjson,
Expand All @@ -24,7 +24,7 @@ const isAuthed = t.middleware(async ({ ctx, next }) => {
return next({
ctx: {
...ctx,
user,
user: user as UserWithSubscription,
},
});
} catch (error) {
Expand All @@ -35,6 +35,32 @@ const isAuthed = t.middleware(async ({ ctx, next }) => {
}
});

const requiresSubscription = t.middleware(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
});
}

const user = ctx.user as UserWithSubscription;

if (!user.isPaidUser || !user.subscription) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Active subscription required",
});
}

return next({
ctx: {
...ctx,
user,
},
});
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure:any = t.procedure.use(isAuthed);
export const protectedProcedure = t.procedure.use(isAuthed) as any;
export const proProcedure = protectedProcedure.use(requiresSubscription) as any;
Comment on lines +65 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/api/src/trpc.ts | head -80

Repository: apsinghdev/opensox

Length of output: 2138


🏁 Script executed:

# Check the Context type definition
rg -A 10 "type Context" apps/api/src/context.ts

# Check how these procedures are used
rg "protectedProcedure|proProcedure" apps/api/src --type ts

Repository: apsinghdev/opensox

Length of output: 1253


🏁 Script executed:

cat -n apps/api/src/context.ts

Repository: apsinghdev/opensox

Length of output: 886


🏁 Script executed:

# Check if there are any type-related imports from @trpc/server
rg "@trpc/server" apps/api/src/trpc.ts -A 5

# Search for ProcedureBuilder or similar type patterns
rg "ProcedureBuilder|Procedure<" apps/api/src --type ts

# Check TRPC version in package.json
cat apps/api/package.json | grep -A 2 "@trpc"

Repository: apsinghdev/opensox

Length of output: 393


🏁 Script executed:

# Get full package.json @trpc section
cat apps/api/package.json | grep -A 20 '"@trpc'

Repository: apsinghdev/opensox

Length of output: 361


🏁 Script executed:

# Search for any TRPC type utilities or middleware patterns in the codebase
rg "middleware|context" apps/api/src --type ts -B 2 -A 2 | head -60

# Check if there are any other procedureBuilders or typed procedures
rg "procedure.*use|Procedure" apps/api/src --type ts | head -20

Repository: apsinghdev/opensox

Length of output: 3926


🏁 Script executed:

# Check how procedures are actually used in one of the routers to see the type impact
cat -n apps/api/src/routers/user.ts | head -50

Repository: apsinghdev/opensox

Length of output: 1517


🏁 Script executed:

# Check if there's any type documentation or comments about this in the codebase
rg "TRPC|middleware|context" apps/api/src --type ts -B 1 | grep -i "type\|TODO\|FIXME" | head -20

# Look for any utility files that might help with typing
fd "type\|util" apps/api/src --type ts

Repository: apsinghdev/opensox

Length of output: 314


🏁 Script executed:

# Search for utils directory
fd -t f "\.ts$" apps/api/src/utils/ | head -20

# Check if there are any typing utilities or helpers
rg "type.*Procedure|Builder" apps/api/src --type-add "ts:*.ts" -t ts

Repository: apsinghdev/opensox

Length of output: 121


🏁 Script executed:

# Let me check the exact TypeScript error by looking for any tsconfig and running a type check
cat apps/api/tsconfig.json

# Also check if there's a build or type check script
cat apps/api/package.json | grep -A 10 '"scripts"'

Repository: apsinghdev/opensox

Length of output: 1514


🏁 Script executed:

# Let me check what specific type issue arises by examining the TRPC types more closely
# First, check if there are any other type-related patterns in the codebase
rg "as.*ProcedureBuilder|as.*Procedure" apps/api/src

# Check the full routers to understand how the context is actually used
cat -n apps/api/src/routers/auth.ts | head -40

Repository: apsinghdev/opensox

Length of output: 1697


🏁 Script executed:

# Let me search for any patterns in the codebase that might show a better way to handle this
rg "ProcedureBuilder|BuildProcedure" apps/api

# Also check if there are any type annotations or helpers defined elsewhere
fd -type f -name "*.ts" -path "*/api/src/*" | xargs grep -l "type.*Procedure" 2>/dev/null

# Check auth.ts to see the UserWithSubscription type definition
cat -n apps/api/src/utils/auth.ts | head -30

Repository: apsinghdev/opensox

Length of output: 1192


🌐 Web query:

TRPC middleware context narrowing type safety @trpc/server

💡 Result:

Short answer

  • Define a broad base context with initTRPC.context().
  • In a middleware, return opts.next({ ctx: { ... } }) with the narrowed properties added/overridden; downstream middlewares/procedures will see the refined type. Use middleware chaining (.use / .pipe / unstable_pipe or .concat for reusable plugins) to compose further narrowing. See tRPC docs for examples. [1][2]

Minimal example (v10/v11 style)

import { initTRPC, TRPCError } from '@trpc/server';
const t = initTRPC.context<{ user?: { id: string } }>().create();

const isAuthed = t.middleware(async (opts) => {
  const { ctx } = opts;
  if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
  // return next with a narrowed ctx (user is now non-nullable)
  return opts.next({ ctx: { user: ctx.user } });
});

const protectedProcedure = t.procedure.use(isAuthed).query(({ ctx }) => {
  // ctx.user is typed as { id: string } (non-nullable) here
  return ctx.user.id;
});

References

  • tRPC middlewares / context extension and middleware chaining. [1]
  • tRPC context typing via initTRPC.context(). [2]

🏁 Script executed:

# Let me check if there's a way to properly type these procedures using TRPC's type system
# Look for any type parameter usage with procedure or middleware
rg "\.use<\|Procedure<" apps/api/src

# Check if there's an initTRPC pattern that allows for proper typing
rg "initTRPC\." apps/api/src -A 3

Repository: apsinghdev/opensox

Length of output: 223


🏁 Script executed:

# Let me check if there's a way to create a custom typed procedure builder
# Look for any advanced patterns or if there's a typesafe alternative
rg "createCallerFactory|createServerSideHelpers" apps/api/src

# Check if there's any documentation or commented code about this
rg "TODO|FIXME|NOTE" apps/api/src/trpc.ts

Repository: apsinghdev/opensox

Length of output: 44


Remove as any casts and properly type context narrowing through middleware.

The as any casts on lines 65-66 violate the type safety guidelines and prevent TypeScript from catching errors in consumers. TRPC v11.7.2 supports proper context narrowing through middleware chaining—refactor to define the narrowed context types explicitly (consider creating a custom typed procedure builder or using TRPC's middleware context propagation without type assertions).

🤖 Prompt for AI Agents
In apps/api/src/trpc.ts around lines 65-66, remove the `as any` casts and
instead surface the narrowed context types from the middlewares: update the
middleware signatures to return a typed context (e.g., isAuthed should return
ctx + { user: User } and requiresSubscription should return ctx + {
subscription: Subscription }), then build typed procedures by using
t.procedure.use(isAuthed) and
t.procedure.use(isAuthed).use(requiresSubscription) while preserving TypeScript
generics so the resulting protectedProcedure and proProcedure inherit the
narrowed ctx types; if helpful create a small typedProcedure builder (or
explicit Procedure type aliases) that supplies the middleware-returned Context
type to avoid assertions and ensure callers get correct compile-time types.

74 changes: 68 additions & 6 deletions apps/api/src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,95 @@
import jwt from "jsonwebtoken";
import prisma from "../prisma.js";
import prismaModule from "../prisma.js";
import { SUBSCRIPTION_STATUS } from "../constants/subscription.js";

const { prisma } = prismaModule;
const JWT_SECRET = process.env.JWT_SECRET! as string;

if (!process.env.JWT_SECRET) {
throw new Error("JWT_SECRET is not defined in the environment variables");
}

export interface UserWithSubscription {
id: string;
email: string;
firstName: string;
authMethod: string;
createdAt: Date;
lastLogin: Date;
completedSteps: any;
isPaidUser: boolean;
subscription: {
id: string;
status: string;
startDate: Date;
endDate: Date | null;
planId: string;
} | null;
}

export const generateToken = (email: string): string => {
return jwt.sign({ email }, JWT_SECRET, { expiresIn: "7d" });
};

export const verifyToken = async (token: string) => {
export const verifyToken = async (token: string): Promise<UserWithSubscription> => {
try {
const decoded = jwt.verify(token, JWT_SECRET);

if (typeof decoded === "string") {
if (typeof decoded === "string" || !decoded || typeof decoded !== "object") {
throw new Error("Invalid token payload");
}

const user = await prisma.prisma.user.findUnique({
where: { email: decoded.email },
const email = (decoded as { email?: string }).email;
if (!email) {
throw new Error("Email not found in token");
}

const user = await prisma.user.findUnique({
where: { email },
include: {
subscriptions: {
where: {
status: SUBSCRIPTION_STATUS.ACTIVE,
endDate: {
gte: new Date(),
},
},
orderBy: {
startDate: "desc",
},
take: 1,
include: {
plan: true,
},
},
},
});

if (!user) {
throw new Error("User not found");
}

return user;
const activeSubscription = user.subscriptions[0] || null;

return {
id: user.id,
email: user.email,
firstName: user.firstName,
authMethod: user.authMethod,
createdAt: user.createdAt,
lastLogin: user.lastLogin,
completedSteps: user.completedSteps,
isPaidUser: !!activeSubscription,
subscription: activeSubscription
? {
id: activeSubscription.id,
status: activeSubscription.status,
startDate: activeSubscription.startDate,
endDate: activeSubscription.endDate,
planId: activeSubscription.planId,
}
: null,
};
} catch (error) {
throw new Error("Token verification failed");
}
Comment on lines 93 to 95
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error context is lost when re-throwing.

The catch block swallows the original error, making debugging difficult. Per coding guidelines, log errors with context for debugging.

   } catch (error) {
+    console.error("Token verification failed:", { error, tokenPrefix: token.substring(0, 10) });
     throw new Error("Token verification failed");
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/api/src/utils/auth.ts around lines 93 to 95, the catch block currently
throws a new Error("Token verification failed") and discards the original error;
change it to preserve and surface the original error by logging it with context
(e.g., auth token verification failed) using the project's logger and either
rethrow the original error or throw a new Error that includes the original error
message/stack so debugging information is retained.

Expand Down
18 changes: 16 additions & 2 deletions apps/web/src/components/checkout/CheckoutWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
"use client";

import React from "react";
import React, { useEffect, useState } from "react";
import { useSubscription } from "@/hooks/useSubscription";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { useSession } from "next-auth/react";
import CheckoutConfirmation from "./checkout-confirmation";

export default function CheckoutWrapper() {
const { isPaidUser, isLoading } = useSubscription();
const router = useRouter();
const searchParams = useSearchParams();
const { data: session, update } = useSession();
const paymentSuccess = searchParams.get("payment") === "success";
const [hasRefreshed, setHasRefreshed] = useState(false);

useEffect(() => {
if (paymentSuccess && session && !hasRefreshed) {
update().then(() => {
setHasRefreshed(true);
router.refresh();
});
}
}, [paymentSuccess, session, update, hasRefreshed, router]);

// Show loading state while checking subscription
if (isLoading) {
Expand Down
Loading