From fc2fc6350004c1decf2fe11c2ff17c4590c0e520 Mon Sep 17 00:00:00 2001 From: Fayzan Naufal Suparjo <68143025+ryoojiz@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:02:38 +0700 Subject: [PATCH] added gumroad integration, updated starter reader --- .env.example | 3 + public/images/logo-black.png | Bin 18924 -> 6287 bytes src/components/Home.tsx | 8 +- src/components/Invitation.tsx | 6 +- src/lib/gumroad.ts | 140 ++++++++++++++++++ .../v1/activation/[[...activationCode]].ts | 140 +++++++++++++----- src/pages/auth/login/index.tsx | 14 ++ src/types.ts | 5 +- xcs-starter.rbxmx | 2 +- 9 files changed, 275 insertions(+), 43 deletions(-) create mode 100644 src/lib/gumroad.ts diff --git a/.env.example b/.env.example index 110a083..6e399fe 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,9 @@ ROBLOX_CLIENT_SECRET="" # Mail SENDGRID_API_KEY= +# Gumroad License Verification +GUMROAD_PRODUCT_ID= + # Firebase Authentication and Image Storage FIREBASE_ADMIN_auth_provider_x509_cert_url= FIREBASE_ADMIN_auth_uri= diff --git a/public/images/logo-black.png b/public/images/logo-black.png index 85fdc632e88df3ba6e341fbe08a9227c7c882939..503a7fb32a58cf0fb6916b7f3917a53b50f0fe02 100644 GIT binary patch literal 6287 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 - What is Restrafes XCS? + What is Wyre Management? - Restrafes XCS is a powerful access control system designed to help you manage access points for your - building. With Restrafes XCS, you can easily and securely control who has access to your property, including + Wyre Management is a powerful access control system designed to help you manage access points for your + building. With Wyre Management, you can easily and securely control who has access to your property, including employees and visitors. The system is highly customizable, allowing you to set access levels and permissions for different users, and offers a range of advanced features such as real-time monitoring, reporting, and reverse-compatibility with other systems. Whether you're looking to enhance the security of your - business or residential property, Restrafes XCS provides the flexibility and reliability you need + business or residential property, Wyre Management provides the flexibility and reliability you need to manage access with confidence. diff --git a/src/components/Invitation.tsx b/src/components/Invitation.tsx index ecace32..f102f77 100644 --- a/src/components/Invitation.tsx +++ b/src/components/Invitation.tsx @@ -61,7 +61,7 @@ export default function Invitation({ invite, errorMessage }: { invite: Invitatio return ( <> - Invitation - Restrafes XCS + Invitation - Wyre Management {'Restrafes - Restrafes XCS + Wyre Management {' '} ; + ip_country: string; + is_gift_receiver_purchase: boolean; + refunded: boolean; + disputed: boolean; + dispute_won: boolean; + is_multiseat_license: boolean; + subscription_ended_at?: string; + subscription_cancelled_at?: string; + subscription_failed_at?: string; + is_recurring_billing: boolean; + can_contact: boolean; + discover_fee_charged: boolean; + }; + message?: string; +} + +export async function verifyGumroadLicense(licenseKey: string): Promise<{ + isValid: boolean; + isActive: boolean; + status: 'active' | 'inactive' | 'unknown'; + message?: string; +}> { + try { + console.log('Verifying Gumroad license key:', licenseKey); + + const productId = process.env.GUMROAD_PRODUCT_ID; + + if (!productId) { + console.error('GUMROAD_PRODUCT_ID is not set in environment variables'); + return { + isValid: false, + isActive: false, + status: 'unknown', + message: 'Server configuration error' + }; + } + + console.log('Using product ID:', productId); + + const response = await fetch('https://api.gumroad.com/v2/licenses/verify', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + product_id: productId, + license_key: licenseKey, + increment_uses_count: 'false' + }) + }); + + console.log('Gumroad API response status:', response.status); + const data: GumroadLicenseResponse = await response.json(); + console.log('Gumroad API response data:', JSON.stringify(data, null, 2)); + + if (!data.success) { + console.log('Gumroad license verification failed:', data.message); + return { + isValid: false, + isActive: false, + status: 'inactive', + message: data.message || 'Invalid license key' + }; + } + + const purchase = data.purchase; + console.log('Purchase details:', JSON.stringify(purchase, null, 2)); + + // Check if the purchase is refunded or disputed + if (purchase.refunded || purchase.disputed) { + console.log('License has been refunded or disputed'); + return { + isValid: true, + isActive: false, + status: 'inactive', + message: 'License has been refunded or disputed' + }; + } + + // Check subscription status for recurring purchases + if (purchase.is_recurring_billing) { + console.log('Checking subscription status for recurring purchase'); + // If subscription has ended, cancelled, or failed + if (purchase.subscription_ended_at || purchase.subscription_cancelled_at || purchase.subscription_failed_at) { + console.log('Subscription has ended, cancelled, or failed:', { + ended_at: purchase.subscription_ended_at, + cancelled_at: purchase.subscription_cancelled_at, + failed_at: purchase.subscription_failed_at + }); + return { + isValid: true, + isActive: false, + status: 'inactive', + message: 'Subscription has ended or been cancelled' + }; + } + } + + // License is valid and active + console.log('License is valid and active'); + return { + isValid: true, + isActive: true, + status: 'active', + message: 'License is valid and active' + }; + + } catch (error) { + console.error('Error verifying Gumroad license:', error); + return { + isValid: false, + isActive: false, + status: 'unknown', + message: 'Error verifying license' + }; + } +} diff --git a/src/pages/api/v1/activation/[[...activationCode]].ts b/src/pages/api/v1/activation/[[...activationCode]].ts index fe84f53..3fa1afc 100644 --- a/src/pages/api/v1/activation/[[...activationCode]].ts +++ b/src/pages/api/v1/activation/[[...activationCode]].ts @@ -1,6 +1,7 @@ import { admin, app } from '@/pages/api/firebase'; import { Invitation, User } from '@/types'; import { NextApiRequest, NextApiResponse } from 'next'; +import { verifyGumroadLicense } from '@/lib/gumroad'; import clientPromise from '@/lib/mongodb'; const sgMail = require('@sendgrid/mail'); @@ -13,34 +14,60 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) let { activationCode } = req.query as { activationCode: string }; activationCode = decodeURIComponent(activationCode); + console.log('Activation attempt with code:', activationCode); + + // First, try to find an invitation code const invitation = (await invitations.findOne({ type: 'xcs', code: activationCode })) as Invitation | null; + let isGumroadLicense = false; + let gumroadVerification = null; + + // If no invitation found, check if it's a Gumroad license key if (!invitation) { - return res.status(404).json({ - valid: false, - message: 'Invalid activation code. Please check the code and try again.' - }); + console.log('No invitation found, checking if it\'s a Gumroad license key'); + gumroadVerification = await verifyGumroadLicense(activationCode); + isGumroadLicense = gumroadVerification.isValid; + + if (!isGumroadLicense) { + console.log('Neither valid invitation nor valid Gumroad license'); + return res.status(404).json({ + valid: false, + message: 'Invalid activation code or license key. Please check the code and try again.' + }); + } + + console.log('Valid Gumroad license found'); + } else { + console.log('Valid invitation found:', invitation.code); } if (req.method === 'GET') { - if (invitation?.maxUses > -1 && invitation?.uses >= invitation?.maxUses) { + // For invitation codes, check max uses + if (invitation && invitation?.maxUses > -1 && invitation?.uses >= invitation?.maxUses) { return res.status(403).json({ valid: false, message: `This activation code has reached its maximum uses.` }); } + // For Gumroad licenses, check if active + if (isGumroadLicense && gumroadVerification && !gumroadVerification.isActive) { + return res.status(403).json({ + valid: false, + message: 'This Gumroad license is not active. Please check your subscription status.' + }); + } + return res.status(200).json({ valid: true, - message: 'Valid activation code.' + message: isGumroadLicense ? 'Valid Gumroad license key.' : 'Valid activation code.' }); - } - - if (req.method === 'POST') { + } if (req.method === 'POST') { let { activationCode } = req.query as { activationCode: string }; + activationCode = decodeURIComponent(activationCode); let { displayName, email, username, password } = req.body as { displayName: string; @@ -49,16 +76,54 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) password: string; }; + console.log('Registration attempt with:', { displayName, email, username, activationCode }); + const mongoClient = await clientPromise; const db = mongoClient.db(process.env.MONGODB_DB as string); const users = db.collection('users'); + const invitations = db.collection('invitations'); - if (invitation.uses > -1 && invitation.uses >= invitation.maxUses) { + // Re-validate the activation code/license for POST request + const postInvitation = (await invitations.findOne({ + type: 'xcs', + code: activationCode + })) as Invitation | null; + + let postIsGumroadLicense = false; + let postGumroadVerification = null; + + // If no invitation found, check if it's a Gumroad license key + if (!postInvitation) { + console.log('No invitation found in POST, checking if it\'s a Gumroad license key'); + postGumroadVerification = await verifyGumroadLicense(activationCode); + postIsGumroadLicense = postGumroadVerification.isValid; + + if (!postIsGumroadLicense) { + console.log('Neither valid invitation nor valid Gumroad license in POST'); + return res.status(404).json({ + message: 'Invalid activation code or license key. Please check the code and try again.' + }); + } + + console.log('Valid Gumroad license found in POST'); + } else { + console.log('Valid invitation found in POST:', postInvitation.code); + } + + // Check invitation-specific limitations + if (postInvitation && postInvitation.maxUses > -1 && postInvitation.uses >= postInvitation.maxUses) { return res.status(403).json({ message: `This activation code has reached its maximum uses.` }); } + // For Gumroad licenses, ensure it's still active + if (postIsGumroadLicense && postGumroadVerification && !postGumroadVerification.isActive) { + return res.status(403).json({ + message: 'This Gumroad license is not active. Please check your subscription status.' + }); + } + // Check body for missing fields and character length if ( // !firstName || @@ -194,12 +259,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) url: `${process.env.NEXT_PUBLIC_ROOT_URL}/home` } } - ], - platform: { + ], platform: { staff: false, staffTitle: null, membership: 0, - invites: invitation.startingReferrals || 0 + invites: postInvitation?.startingReferrals || 0 }, payment: { customerId: null @@ -220,24 +284,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) referrals: 0, scans: 0 }, - achievements: {}, - - sponsorId: invitation.isSponsor ? invitation.createdBy : null, + achievements: {}, license_key: postIsGumroadLicense ? activationCode : '', + gumroad_status: postIsGumroadLicense ? 'active' as const : 'unknown' as const, + sponsorId: postInvitation?.isSponsor ? postInvitation.createdBy : null, createdAt: new Date(), updatedAt: new Date() - } as User) - .then(async (result) => { - // max uses - if (invitation.maxUses > -1 && invitation.uses + 1 >= invitation.maxUses) { - await invitations.deleteOne({ - code: activationCode[0] - }); - } else { - await invitations.updateOne({ code: activationCode[0] }, { $inc: { uses: 1 } }); + } as User) .then(async (result) => { + // Only handle invitation-specific logic if this was an invitation code + if (postInvitation) { + // max uses + if (postInvitation.maxUses > -1 && postInvitation.uses + 1 >= postInvitation.maxUses) { + await invitations.deleteOne({ + code: activationCode + }); + } else { + await invitations.updateOne({ code: activationCode }, { $inc: { uses: 1 } }); + } + // sponsors + if (postInvitation.isSponsor) { + await users.updateOne({ id: postInvitation.createdBy }, { $inc: { 'platform.invites': -1 } }); + } } - // sponsors - if (invitation.isSponsor) { - await users.updateOne({ id: invitation.createdBy }, { $inc: { 'platform.invites': -1 } }); + console.log('User created successfully:', firebaseUser.uid); + + if (postIsGumroadLicense) { + console.log('User registered with Gumroad license:', activationCode); + } else { + console.log('User registered with invitation code:', activationCode); } }) .catch((error) => { @@ -277,11 +350,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); } catch (error) { console.log(error); - } - - return res.status(200).json({ - message: 'Successfully registered! You may now login.', - success: true + } return res.status(200).json({ + message: postIsGumroadLicense + ? 'Successfully registered with Gumroad license! You may now login.' + : 'Successfully registered with invitation code! You may now login.', + success: true, + registrationType: postIsGumroadLicense ? 'gumroad' : 'invitation' }); } diff --git a/src/pages/auth/login/index.tsx b/src/pages/auth/login/index.tsx index 5b4f140..889321b 100644 --- a/src/pages/auth/login/index.tsx +++ b/src/pages/auth/login/index.tsx @@ -524,6 +524,20 @@ export default function Login() { variant={'outline'} /> + + Don't have an activation code?{' '} + + + Purchase a license here. + + {' '} + )} diff --git a/src/types.ts b/src/types.ts index 18c346a..72e32a4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -48,9 +48,10 @@ export interface User { referrals: number; scans: number; organizationInvitations?: number; - }; - achievements?: Record; + }; achievements?: Record; organizations?: Organization[]; + license_key: string; + gumroad_status: 'active' | 'inactive' | 'unknown'; } export interface Organization { diff --git a/xcs-starter.rbxmx b/xcs-starter.rbxmx index 1d57db5..7c3e386 100644 --- a/xcs-starter.rbxmx +++ b/xcs-starter.rbxmx @@ -74,7 +74,7 @@ To configure access points for this location, visit: --]] local settings = { - ["url"] = "https://xcs.restrafes.co/api/v1/ingame"; + ["url"] = "https://wyre.ryj.my.id/api/v1/ingame"; ["placeId"] = "{{locationId}}"; ["apiKey"] = "{{apiKey}}";