added gumroad integration, updated starter reader

This commit is contained in:
Fayzan Naufal Suparjo 2025-06-13 20:02:38 +07:00
parent f5865928a8
commit fc2fc63500
9 changed files with 275 additions and 43 deletions

View file

@ -30,6 +30,9 @@ ROBLOX_CLIENT_SECRET=""
# Mail # Mail
SENDGRID_API_KEY= SENDGRID_API_KEY=
# Gumroad License Verification
GUMROAD_PRODUCT_ID=
# Firebase Authentication and Image Storage # Firebase Authentication and Image Storage
FIREBASE_ADMIN_auth_provider_x509_cert_url= FIREBASE_ADMIN_auth_provider_x509_cert_url=
FIREBASE_ADMIN_auth_uri= FIREBASE_ADMIN_auth_uri=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -94,15 +94,15 @@ export default function Home({ allPostsData: posts }: { allPostsData: any }) {
fontWeight={'400'} fontWeight={'400'}
pb={2} pb={2}
> >
What is Restrafes XCS? What is Wyre Management?
</Heading> </Heading>
<Text fontSize={'xl'}> <Text fontSize={'xl'}>
Restrafes XCS is a powerful access control system designed to help you manage access points for your Wyre Management 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 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 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 for different users, and offers a range of advanced features such as real-time monitoring, reporting, and
reverse-compatibility with other systems. Whether you&apos;re looking to enhance the security of your reverse-compatibility with other systems. Whether you&apos;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. to manage access with confidence.
</Text> </Text>
</Box> </Box>

View file

@ -61,7 +61,7 @@ export default function Invitation({ invite, errorMessage }: { invite: Invitatio
return ( return (
<> <>
<Head> <Head>
<title>Invitation - Restrafes XCS</title> <title>Invitation - Wyre Management</title>
</Head> </Head>
<Container <Container
maxW={'container.lg'} maxW={'container.lg'}
@ -81,7 +81,7 @@ export default function Invitation({ invite, errorMessage }: { invite: Invitatio
> >
<Image <Image
src={useColorModeValue('/images/logo-black.png', '/images/logo-white.png')} src={useColorModeValue('/images/logo-black.png', '/images/logo-white.png')}
alt={'Restrafes XCS Logo'} alt={'Wyre Management Logo'}
w={'auto'} w={'auto'}
h={'24px'} h={'24px'}
objectFit={'contain'} objectFit={'contain'}
@ -243,7 +243,7 @@ export default function Invitation({ invite, errorMessage }: { invite: Invitatio
as={'span'} as={'span'}
fontWeight={'bold'} fontWeight={'bold'}
> >
Restrafes XCS Wyre Management
</Text>{' '} </Text>{' '}
<Text <Text
as={'span'} as={'span'}

140
src/lib/gumroad.ts Normal file
View file

@ -0,0 +1,140 @@
interface GumroadLicenseResponse {
success: boolean;
uses: number;
purchase: {
product_name: string;
product_id: string;
created_at: string;
full_name: string;
purchaser_id: string;
product_permalink: string;
id: string;
variants: string;
test: boolean;
status: string;
subscription_id?: string;
licence_key: string;
quantity: number;
gumroad_fee: number;
currency: string;
order_number: number;
sale_id: string;
sale_timestamp: string;
url_params: Record<string, any>;
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'
};
}
}

View file

@ -1,6 +1,7 @@
import { admin, app } from '@/pages/api/firebase'; import { admin, app } from '@/pages/api/firebase';
import { Invitation, User } from '@/types'; import { Invitation, User } from '@/types';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import { verifyGumroadLicense } from '@/lib/gumroad';
import clientPromise from '@/lib/mongodb'; import clientPromise from '@/lib/mongodb';
const sgMail = require('@sendgrid/mail'); 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 }; let { activationCode } = req.query as { activationCode: string };
activationCode = decodeURIComponent(activationCode); activationCode = decodeURIComponent(activationCode);
console.log('Activation attempt with code:', activationCode);
// First, try to find an invitation code
const invitation = (await invitations.findOne({ const invitation = (await invitations.findOne({
type: 'xcs', type: 'xcs',
code: activationCode code: activationCode
})) as Invitation | null; })) as Invitation | null;
let isGumroadLicense = false;
let gumroadVerification = null;
// If no invitation found, check if it's a Gumroad license key
if (!invitation) { if (!invitation) {
return res.status(404).json({ console.log('No invitation found, checking if it\'s a Gumroad license key');
valid: false, gumroadVerification = await verifyGumroadLicense(activationCode);
message: 'Invalid activation code. Please check the code and try again.' 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 (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({ return res.status(403).json({
valid: false, valid: false,
message: `This activation code has reached its maximum uses.` 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({ return res.status(200).json({
valid: true, 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 }; let { activationCode } = req.query as { activationCode: string };
activationCode = decodeURIComponent(activationCode);
let { displayName, email, username, password } = req.body as { let { displayName, email, username, password } = req.body as {
displayName: string; displayName: string;
@ -49,16 +76,54 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
password: string; password: string;
}; };
console.log('Registration attempt with:', { displayName, email, username, activationCode });
const mongoClient = await clientPromise; const mongoClient = await clientPromise;
const db = mongoClient.db(process.env.MONGODB_DB as string); const db = mongoClient.db(process.env.MONGODB_DB as string);
const users = db.collection('users'); 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({ return res.status(403).json({
message: `This activation code has reached its maximum uses.` 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 // Check body for missing fields and character length
if ( if (
// !firstName || // !firstName ||
@ -194,12 +259,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
url: `${process.env.NEXT_PUBLIC_ROOT_URL}/home` url: `${process.env.NEXT_PUBLIC_ROOT_URL}/home`
} }
} }
], ], platform: {
platform: {
staff: false, staff: false,
staffTitle: null, staffTitle: null,
membership: 0, membership: 0,
invites: invitation.startingReferrals || 0 invites: postInvitation?.startingReferrals || 0
}, },
payment: { payment: {
customerId: null customerId: null
@ -220,24 +284,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
referrals: 0, referrals: 0,
scans: 0 scans: 0
}, },
achievements: {}, achievements: {}, license_key: postIsGumroadLicense ? activationCode : '',
gumroad_status: postIsGumroadLicense ? 'active' as const : 'unknown' as const,
sponsorId: invitation.isSponsor ? invitation.createdBy : null, sponsorId: postInvitation?.isSponsor ? postInvitation.createdBy : null,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date()
} as User) } as User) .then(async (result) => {
.then(async (result) => { // Only handle invitation-specific logic if this was an invitation code
// max uses if (postInvitation) {
if (invitation.maxUses > -1 && invitation.uses + 1 >= invitation.maxUses) { // max uses
await invitations.deleteOne({ if (postInvitation.maxUses > -1 && postInvitation.uses + 1 >= postInvitation.maxUses) {
code: activationCode[0] await invitations.deleteOne({
}); code: activationCode
} else { });
await invitations.updateOne({ code: activationCode[0] }, { $inc: { uses: 1 } }); } else {
await invitations.updateOne({ code: activationCode }, { $inc: { uses: 1 } });
}
// sponsors
if (postInvitation.isSponsor) {
await users.updateOne({ id: postInvitation.createdBy }, { $inc: { 'platform.invites': -1 } });
}
} }
// sponsors console.log('User created successfully:', firebaseUser.uid);
if (invitation.isSponsor) {
await users.updateOne({ id: invitation.createdBy }, { $inc: { 'platform.invites': -1 } }); if (postIsGumroadLicense) {
console.log('User registered with Gumroad license:', activationCode);
} else {
console.log('User registered with invitation code:', activationCode);
} }
}) })
.catch((error) => { .catch((error) => {
@ -277,11 +350,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}); });
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} } return res.status(200).json({
message: postIsGumroadLicense
return res.status(200).json({ ? 'Successfully registered with Gumroad license! You may now login.'
message: 'Successfully registered! You may now login.', : 'Successfully registered with invitation code! You may now login.',
success: true success: true,
registrationType: postIsGumroadLicense ? 'gumroad' : 'invitation'
}); });
} }

View file

@ -524,6 +524,20 @@ export default function Login() {
variant={'outline'} variant={'outline'}
/> />
</InputGroup> </InputGroup>
<Text fontSize={'sm'}>
Don't have an activation code?{' '}
<Text as={'span'}>
<Link
as={NextLink}
href={'https://amperra.gumroad.com/l/wyre'}
textDecor={'underline'}
textUnderlineOffset={4}
whiteSpace={'nowrap'}
>
Purchase a license here.
</Link>
</Text>{' '}
</Text>
</FormControl> </FormControl>
)} )}
</Field> </Field>

View file

@ -48,9 +48,10 @@ export interface User {
referrals: number; referrals: number;
scans: number; scans: number;
organizationInvitations?: number; organizationInvitations?: number;
}; }; achievements?: Record<string, Achievement>;
achievements?: Record<string, Achievement>;
organizations?: Organization[]; organizations?: Organization[];
license_key: string;
gumroad_status: 'active' | 'inactive' | 'unknown';
} }
export interface Organization { export interface Organization {

View file

@ -74,7 +74,7 @@ To configure access points for this location, visit:
--]] --]]
local settings = { local settings = {
["url"] = "https://xcs.restrafes.co/api/v1/ingame"; ["url"] = "https://wyre.ryj.my.id/api/v1/ingame";
["placeId"] = "{{locationId}}"; ["placeId"] = "{{locationId}}";
["apiKey"] = "{{apiKey}}"; ["apiKey"] = "{{apiKey}}";