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
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=

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'}
pb={2}
>
What is Restrafes XCS?
What is Wyre Management?
</Heading>
<Text fontSize={'xl'}>
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&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.
</Text>
</Box>

View file

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

View file

@ -524,6 +524,20 @@ export default function Login() {
variant={'outline'}
/>
</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>
)}
</Field>

View file

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

View file

@ -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}}";