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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createBillingPortalSession, createCheckoutSession, createCustomer, isTi
import { and, eq, isNull } from 'drizzle-orm';
import { headers } from 'next/headers';
import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '../../trpc';
import { createTRPCRouter, optionalAuthProcedure, protectedProcedure } from '../../trpc';

export const subscriptionRouter = createTRPCRouter({
getLegacySubscriptions: protectedProcedure.query(async ({ ctx }) => {
Expand Down Expand Up @@ -45,6 +45,34 @@ export const subscriptionRouter = createTRPCRouter({

return fromDbSubscription(subscription, scheduledPrice);
}),
getOptional: optionalAuthProcedure.query(async ({ ctx }) => {
if (!ctx.user) {
return null;
}
const subscription = await ctx.db.query.subscriptions.findFirst({
where: and(
eq(subscriptions.userId, ctx.user.id),
eq(subscriptions.status, SubscriptionStatus.ACTIVE),
),
with: {
product: true,
price: true,
},
});

if (!subscription) {
return null;
}

let scheduledPrice = null;
if (subscription.scheduledPriceId) {
scheduledPrice = await ctx.db.query.prices.findFirst({
where: eq(prices.id, subscription.scheduledPriceId),
}) ?? null;
}

return fromDbSubscription(subscription, scheduledPrice);
}),
getPriceId: protectedProcedure.input(z.object({
priceKey: z.nativeEnum(PriceKey),
})).mutation(async ({ input, ctx }) => {
Expand Down
22 changes: 21 additions & 1 deletion apps/web/client/src/server/api/routers/user/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { extractNames } from '@onlook/utility';
import type { User as SupabaseUser } from "@supabase/supabase-js";
import { eq } from 'drizzle-orm';
import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '../../trpc';
import { createTRPCRouter, optionalAuthProcedure, protectedProcedure } from '../../trpc';
import { userSettingsRouter } from './user-settings';

export const userRouter = createTRPCRouter({
Expand All @@ -26,6 +26,26 @@ export const userRouter = createTRPCRouter({
}) : null;
return userData;
}),
getOptional: optionalAuthProcedure.query(async ({ ctx }) => {
if (!ctx.user) {
return null;
}
const authUser = ctx.user;
const user = await ctx.db.query.users.findFirst({
where: eq(users.id, authUser.id),
});

const { displayName, firstName, lastName } = getUserName(authUser);
const userData = user ? fromDbUser({
...user,
firstName: user.firstName ?? firstName,
lastName: user.lastName ?? lastName,
displayName: user.displayName ?? displayName,
email: user.email ?? authUser.email,
avatarUrl: user.avatarUrl ?? authUser.user_metadata.avatarUrl,
}) : null;
return userData;
}),
getById: protectedProcedure.input(z.string()).query(async ({ ctx, input }) => {
const user = await ctx.db.query.users.findFirst({
where: eq(users.id, input),
Expand Down
32 changes: 26 additions & 6 deletions apps/web/client/src/server/api/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ import { ZodError } from 'zod';
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
const supabase = await createClient();
const {
data: { user },
error,
} = await supabase.auth.getUser();

if (error) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: error.message });
// Attempt to get the user, but don't throw on auth errors.
// Unauthenticated visitors (e.g., marketing pages) should still get a valid
// context with user set to null. Protected procedures validate auth separately.
let user: User | null = null;
const { data, error } = await supabase.auth.getUser();
if (!error && data.user) {
user = data.user;
}

return {
Expand Down Expand Up @@ -149,6 +150,25 @@ export const protectedProcedure = t.procedure.use(timingMiddleware).use(({ ctx,
});
});

/**
* Optional auth procedure
*
* Use this for endpoints where authentication is optional. The user context is
* passed through as-is (may be null for unauthenticated visitors). Components
* like telemetry providers, pricing tables, and auth buttons on marketing pages
* should use this instead of protectedProcedure.
*
* @see https://trpc.io/docs/procedures
*/
export const optionalAuthProcedure = t.procedure.use(timingMiddleware).use(({ ctx, next }) => {
return next({
ctx: {
user: ctx.user ?? null,
db: ctx.db,
},
});
});

/**
* Admin procedure with service role access
*
Expand Down