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 85fdc63..503a7fb 100644 Binary files a/public/images/logo-black.png and b/public/images/logo-black.png differ diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 244f273..a65a504 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -94,15 +94,15 @@ export default function Home({ allPostsData: posts }: { allPostsData: any }) { fontWeight={'400'} pb={2} > - 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}}";