mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-23 02:31:31 +00:00
Empties ADMIN_EMAILS and DEVELOPER_GITHUB_USERNAMES, so the impersonate, recent-purchases and whitelist routes answer 403 to every caller. Points security reports and funding at Hanzo.
161 lines
4.8 KiB
TypeScript
161 lines
4.8 KiB
TypeScript
/**
|
|
* This file contains the whitelist of users who are allowed to access the subscription features.
|
|
*/
|
|
import type { UserContextType } from '~/features/auth/types'
|
|
import {
|
|
ProductName,
|
|
type ProductNameType,
|
|
SubscriptionStatus,
|
|
type UserSubscriptionStatus,
|
|
} from '~/shared/types/subscription'
|
|
|
|
/**
|
|
* GitHub usernames allowed to test the purchase flow while holding Pro. These
|
|
* users bypass the active-subscription check, unlock the DEV_TEST_99 coupon
|
|
* (99% off), and may repurchase the same domain. Empty, so no account
|
|
* qualifies.
|
|
*/
|
|
export const DEVELOPER_GITHUB_USERNAMES: readonly string[] = []
|
|
|
|
/**
|
|
* Check if a subscription has specific product access
|
|
*/
|
|
const hasProductAccess = (
|
|
subscriptions: UserContextType['subscriptions'],
|
|
productName: ProductNameType
|
|
): boolean => {
|
|
return (
|
|
subscriptions?.some(
|
|
(sub) =>
|
|
(sub.status === SubscriptionStatus.Active ||
|
|
sub.status === SubscriptionStatus.Trialing) &&
|
|
sub.subscription_items?.some((item) => item.price?.product?.name === productName)
|
|
) ?? false
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Calculate support tier from subscription data
|
|
* V2 support tiers: direct = 1, sponsor = 2
|
|
* V1 support tier: based on quantity
|
|
*/
|
|
const calculateSupportTier = (
|
|
subscriptions: UserContextType['subscriptions']
|
|
): number => {
|
|
if (!subscriptions) return 0
|
|
|
|
// Check V2 sponsor support first (highest tier)
|
|
const hasSponsor = subscriptions.some(
|
|
(sub) =>
|
|
sub.subscription_items?.some(
|
|
(item) => item.price?.product?.name === ProductName.GuiSupportSponsor
|
|
) &&
|
|
(sub.status === SubscriptionStatus.Active ||
|
|
sub.status === SubscriptionStatus.Trialing)
|
|
)
|
|
if (hasSponsor) return 2
|
|
|
|
// Check V2 direct support
|
|
const hasDirect = subscriptions.some(
|
|
(sub) =>
|
|
sub.subscription_items?.some(
|
|
(item) => item.price?.product?.name === ProductName.GuiSupportDirect
|
|
) &&
|
|
(sub.status === SubscriptionStatus.Active ||
|
|
sub.status === SubscriptionStatus.Trialing)
|
|
)
|
|
if (hasDirect) return 1
|
|
|
|
// V1 support (legacy)
|
|
const supportItem = subscriptions.find((sub) =>
|
|
sub.subscription_items?.some(
|
|
(item) =>
|
|
item.price?.product?.name === ProductName.GuiSupport &&
|
|
(sub.status === SubscriptionStatus.Active ||
|
|
sub.status === SubscriptionStatus.Trialing)
|
|
)
|
|
)
|
|
|
|
return supportItem?.quantity ?? 0
|
|
}
|
|
|
|
const calculateTeamSeats = (subscriptions: UserContextType['subscriptions']): number => {
|
|
if (!subscriptions) return 0
|
|
|
|
return (
|
|
subscriptions.find((sub) =>
|
|
sub.subscription_items?.some(
|
|
(item) =>
|
|
item.price?.product?.name === ProductName.GuiProTeamSeats &&
|
|
(sub.status === SubscriptionStatus.Active ||
|
|
sub.status === SubscriptionStatus.Trialing)
|
|
)
|
|
)?.quantity ?? 0
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Check if a user is a developer who can test the purchase flow
|
|
*/
|
|
const isDeveloperUser = (userData?: UserContextType): boolean => {
|
|
if (!userData?.githubUsername) return false
|
|
return DEVELOPER_GITHUB_USERNAMES.includes(userData.githubUsername.toLowerCase())
|
|
}
|
|
|
|
/**
|
|
* Get the subscription status of a user.
|
|
*/
|
|
export const userSubscriptionStatus = (
|
|
userData?: UserContextType
|
|
): UserSubscriptionStatus => {
|
|
if (!userData)
|
|
return {
|
|
pro: false,
|
|
proV1: false,
|
|
proV2: false,
|
|
chat: false,
|
|
supportTier: 0,
|
|
teamSeats: 0,
|
|
couponCodes: { previouslySubscribed: 'RENEWAL30' },
|
|
isDeveloper: false,
|
|
}
|
|
|
|
const hasV1Pro =
|
|
hasProductAccess(userData.subscriptions, ProductName.GuiPro) ||
|
|
hasProductAccess(userData.subscriptions, ProductName.GuiProTeamSeats)
|
|
|
|
const hasV2Pro =
|
|
hasProductAccess(userData.subscriptions, ProductName.GuiProV2) ||
|
|
hasProductAccess(userData.subscriptions, ProductName.GuiProV2Upgrade) ||
|
|
// V2 support tiers also imply Pro access (support is add-on to Pro license)
|
|
hasProductAccess(userData.subscriptions, ProductName.GuiSupportDirect) ||
|
|
hasProductAccess(userData.subscriptions, ProductName.GuiSupportSponsor)
|
|
|
|
const isPro = hasV1Pro || hasV2Pro
|
|
|
|
const isChat =
|
|
// V1 chat
|
|
hasProductAccess(userData.subscriptions, ProductName.GuiChat) ||
|
|
// V2 Pro includes basic chat support
|
|
hasProductAccess(userData.subscriptions, ProductName.GuiProV2) ||
|
|
hasProductAccess(userData.subscriptions, ProductName.GuiProV2Upgrade) ||
|
|
// V2 support tiers include chat
|
|
hasProductAccess(userData.subscriptions, ProductName.GuiSupportDirect) ||
|
|
hasProductAccess(userData.subscriptions, ProductName.GuiSupportSponsor)
|
|
|
|
const supportTier = calculateSupportTier(userData.subscriptions) || 0
|
|
|
|
return {
|
|
pro: isPro,
|
|
proV1: hasV1Pro,
|
|
proV2: hasV2Pro,
|
|
chat: isChat,
|
|
supportTier,
|
|
teamSeats: calculateTeamSeats(userData.subscriptions),
|
|
couponCodes: {
|
|
previouslySubscribed: 'RENEWAL30',
|
|
},
|
|
isDeveloper: isDeveloperUser(userData),
|
|
}
|
|
}
|