diff --git a/.env.example b/.env.example index f3f42634..e9e868f0 100644 --- a/.env.example +++ b/.env.example @@ -27,7 +27,6 @@ YANDEX_CLIENT_ID= YANDEX_CLIENT_SECRET= # AI secrets -OPEN_AI_ORG= OPEN_AI_TOKEN= # Databases diff --git a/actions/auth/get-current-user.ts b/actions/auth/get-current-user.ts index 4d3bd1a8..be8aa39b 100644 --- a/actions/auth/get-current-user.ts +++ b/actions/auth/get-current-user.ts @@ -2,7 +2,7 @@ import { getServerSession } from 'next-auth/next'; -import { authOptions } from '@/lib/auth'; +import { authOptions } from '@/lib/auth/auth'; export const getCurrentUser = async () => { const session = await getServerSession(authOptions); diff --git a/actions/auth/get-updated-user.ts b/actions/auth/get-updated-user.ts new file mode 100644 index 00000000..f4e52da8 --- /dev/null +++ b/actions/auth/get-updated-user.ts @@ -0,0 +1,22 @@ +'use server'; + +import { TEN_MINUTE_SEC } from '@/constants/common'; +import { fetchCachedData } from '@/lib/cache'; +import { db } from '@/lib/db'; + +export const getUpdatedUser = async (userId = '') => { + const updatedUser = await fetchCachedData( + `updated-user-[${userId}]`, + async () => { + const updatedUser = await db.user.findUnique({ + where: { id: userId }, + select: { role: true }, + }); + + return { role: updatedUser?.role }; + }, + TEN_MINUTE_SEC, + ); + + return updatedUser; +}; diff --git a/actions/auth/login-user.ts b/actions/auth/login-user.ts index e6dc08d4..60d8a335 100644 --- a/actions/auth/login-user.ts +++ b/actions/auth/login-user.ts @@ -10,10 +10,14 @@ export const loginUser = async ( name?: string | null, pictureUrl?: string | null, ) => { - const existingUser = await db.user.findUnique({ where: { email } }); + const existingUser = await db.user.findUnique({ + where: { email }, + include: { stripeSubscription: true }, + }); if (existingUser) { return { + hasSubscription: Boolean(existingUser.stripeSubscription), id: existingUser.id, image: existingUser.pictureUrl, isPublic: existingUser.isPublic, @@ -80,6 +84,7 @@ export const loginUser = async ( }); return { + hasSubscription: false, id: user.id, image: user.pictureUrl, isPublic: user.isPublic, diff --git a/actions/stripe/get-user-subscription.ts b/actions/stripe/get-user-subscription.ts new file mode 100644 index 00000000..1a8d9383 --- /dev/null +++ b/actions/stripe/get-user-subscription.ts @@ -0,0 +1,70 @@ +'use server'; + +import { StripeSubscriptionPeriod } from '@prisma/client'; +import { compareAsc, fromUnixTime } from 'date-fns'; + +import { ONE_DAY_SEC } from '@/constants/common'; +import { fetchCachedData } from '@/lib/cache'; +import { db } from '@/lib/db'; +import { stripe } from '@/server/stripe'; + +export const getUserSubscription = async (userId = '', noCache = false) => { + try { + const callback = async () => { + const userSubscription = await db.stripeSubscription.findUnique({ + where: { userId }, + }); + + if (!userSubscription) { + return null; + } + + const stripeSubscription = await stripe.subscriptions.retrieve( + userSubscription.stripeSubscriptionId, + ); + + if (!stripeSubscription) { + return null; + } + + if ( + stripeSubscription.cancel_at && + compareAsc(fromUnixTime(stripeSubscription.cancel_at), Date.now()) < 0 + ) { + await db.stripeSubscription.delete({ + where: { stripeSubscriptionId: stripeSubscription.id }, + }); + + return null; + } + + const planDescription = await db.stripeSubscriptionDescription.findFirst({ + where: { + period: `${stripeSubscription.items.data[0].plan.interval}ly` as StripeSubscriptionPeriod, + }, + }); + + return { + cancelAt: stripeSubscription.cancel_at ? fromUnixTime(stripeSubscription.cancel_at) : null, + endPeriod: fromUnixTime(stripeSubscription.current_period_end), + price: { + currency: stripeSubscription.items.data[0].price.currency, + unitAmount: stripeSubscription.items.data[0].price.unit_amount, + }, + plan: stripeSubscription.items.data[0].plan, + planName: planDescription?.name ?? 'Nova Plus', + startPeriod: fromUnixTime(stripeSubscription.current_period_start), + }; + }; + + const subscription = noCache + ? await callback() + : await fetchCachedData(`user-subscription-[${userId}]`, callback, ONE_DAY_SEC); + + return subscription; + } catch (error) { + console.error('[GET_USER_SUBSCRIPTION]', error); + + return null; + } +}; diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx index bdb0334f..0a0c5dcc 100644 --- a/app/(chat)/layout.tsx +++ b/app/(chat)/layout.tsx @@ -5,7 +5,6 @@ import { getCurrentUser } from '@/actions/auth/get-current-user'; import { getGlobalProgress } from '@/actions/courses/get-global-progress'; import { getUserNotifications } from '@/actions/users/get-user-notifications'; import { NavBar } from '@/components/navbar/navbar'; -import { UserRole } from '@/constants/auth'; export const metadata: Metadata = { title: 'Chat AI', @@ -24,7 +23,7 @@ const ChatLayout = async ({ children }: ChatLayoutProps) => { take: 5, }); - if (![UserRole.ADMIN, UserRole.TEACHER].includes(user?.role as UserRole)) { + if (!user?.hasSubscription) { return redirect('/'); } diff --git a/app/(dashboard)/(routes)/landing-course/[courseId]/page.tsx b/app/(dashboard)/(routes)/landing-course/[courseId]/page.tsx index e6a51b16..1de5e2ea 100644 --- a/app/(dashboard)/(routes)/landing-course/[courseId]/page.tsx +++ b/app/(dashboard)/(routes)/landing-course/[courseId]/page.tsx @@ -118,8 +118,6 @@ const LandingCourseIdPage = async ({ params }: LandingCourseIdPageProps) => { )} - {/* TODO: External recourses. [https://trello.com/c/R4RkoqmC/13-add-external-resources-for-landing-course-page] */} - {/*
*/} diff --git a/app/(dashboard)/(routes)/leaderboard/_components/leaders-table.tsx b/app/(dashboard)/(routes)/leaderboard/_components/leaders-table.tsx index 5bd89df1..846fba30 100644 --- a/app/(dashboard)/(routes)/leaderboard/_components/leaders-table.tsx +++ b/app/(dashboard)/(routes)/leaderboard/_components/leaders-table.tsx @@ -22,10 +22,11 @@ type LeadersTableProps = { userId: string; xp: number; }[]; + hasSubscription?: boolean; userId?: string; }; -export const LeadersTable = ({ leaders, userId }: LeadersTableProps) => { +export const LeadersTable = ({ leaders, hasSubscription = false, userId }: LeadersTableProps) => { const filteredLeaders = leaders.filter((leader) => leader.userId !== userId); const currentLeader = leaders.find((leader) => leader.userId === userId); @@ -54,11 +55,12 @@ export const LeadersTable = ({ leaders, userId }: LeadersTableProps) => {{leader.name}
{leader.userId === userId &&Active Plan
+ {userSubscription?.cancelAt && ( +{userSubscription.planName}
++ Your subscription will be {userSubscription?.cancelAt ? 'cancelled' : 'renewed'} on{' '} + {format(userSubscription.endPeriod, TIMESTAMP_SUBSCRIPTION_TEMPLATE)} +
+You do not have any active subscriptions.
+Billing History
+- Unlock premium courses, get access to Nova AI, and more. -
- -+ Unlock premium courses, get access to Nova AI, and more. +
+ +