Skip to content

Commit 1277be6

Browse files
committed
all the changes
1 parent 4a79881 commit 1277be6

File tree

12 files changed

+733
-17
lines changed

12 files changed

+733
-17
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
UPSTASH_REDIS_REST_URL=your_upstash_redis_rest_url_here
2+
UPSTASH_REDIS_REST_TOKEN=your_upstash_redis_rest_token_here
3+
STRIPE_SECRET_KEY=your_stripe_secret_key_here

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
.DS_Store
1+
.DS_Store
2+
.env.local
3+
.env
4+
node_modules/
5+
.vercel

api/create-checkout.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Stripe from 'stripe';
2+
import { getRedisClient } from './lib/redis.js';
3+
4+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
5+
6+
export default async function handler(req, res) {
7+
if (req.method !== 'POST') {
8+
return res.status(405).json({ error: 'Method not allowed' });
9+
}
10+
11+
try {
12+
const { email, attestationData } = req.body;
13+
14+
if (!email) {
15+
return res.status(400).json({ error: 'Email is required' });
16+
}
17+
18+
const redis = getRedisClient();
19+
20+
// Check if user already has lifetime access
21+
const existingKey = await redis.get(`api_key:email:${email}`);
22+
if (existingKey) {
23+
return res.status(200).json({
24+
alreadyPurchased: true,
25+
apiKey: existingKey,
26+
message: 'You already have lifetime access!'
27+
});
28+
}
29+
30+
// Create Stripe checkout session
31+
const session = await stripe.checkout.sessions.create({
32+
payment_method_types: ['card'],
33+
line_items: [
34+
{
35+
price_data: {
36+
currency: 'usd',
37+
product_data: {
38+
name: 'attest.ink Lifetime Short URLs',
39+
description: 'Create unlimited permanent short URLs for your AI attestations',
40+
images: ['https://attest.ink/assets/logo/circular-2-ai.svg'],
41+
},
42+
unit_amount: 2000, // $20.00
43+
},
44+
quantity: 1,
45+
},
46+
],
47+
mode: 'payment',
48+
success_url: `${process.env.VERCEL_URL || 'https://attest.ink'}/payment-success?session_id={CHECKOUT_SESSION_ID}`,
49+
cancel_url: `${process.env.VERCEL_URL || 'https://attest.ink'}/create/`,
50+
customer_email: email,
51+
metadata: {
52+
email: email,
53+
attestationData: attestationData ? JSON.stringify(attestationData) : ''
54+
},
55+
});
56+
57+
res.status(200).json({ checkoutUrl: session.url });
58+
} catch (error) {
59+
console.error('Error creating checkout session:', error);
60+
res.status(500).json({ error: 'Failed to create checkout session' });
61+
}
62+
}

api/lib/redis.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Redis } from '@upstash/redis';
2+
3+
let redis;
4+
5+
export function getRedisClient() {
6+
if (!redis) {
7+
const url = process.env.UPSTASH_REDIS_REST_URL;
8+
const token = process.env.UPSTASH_REDIS_REST_TOKEN;
9+
10+
if (!url || !token) {
11+
throw new Error('Missing UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN environment variables');
12+
}
13+
14+
redis = new Redis({
15+
url,
16+
token,
17+
});
18+
}
19+
20+
return redis;
21+
}

api/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "attest-ink-api",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"dependencies": {
6+
"@upstash/redis": "^1.28.0",
7+
"nanoid": "^5.0.4",
8+
"stripe": "^14.14.0"
9+
}
10+
}

api/s/[id].js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { getRedisClient } from '../lib/redis.js';
2+
3+
export default async function handler(req, res) {
4+
if (req.method !== 'GET') {
5+
return res.status(405).json({ error: 'Method not allowed' });
6+
}
7+
8+
try {
9+
const { id } = req.query;
10+
11+
if (!id) {
12+
return res.status(400).json({ error: 'Missing short ID' });
13+
}
14+
15+
const redis = getRedisClient();
16+
const dataUrl = await redis.get(`url:${id}`);
17+
18+
if (!dataUrl) {
19+
return res.status(404).json({ error: 'Short URL not found' });
20+
}
21+
22+
res.status(200).json({ dataUrl });
23+
} catch (error) {
24+
console.error('Error retrieving URL:', error);
25+
res.status(500).json({ error: 'Failed to retrieve URL' });
26+
}
27+
}

api/shorten.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { getRedisClient } from './lib/redis.js';
2+
import { customAlphabet } from 'nanoid';
3+
4+
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 8);
5+
6+
export default async function handler(req, res) {
7+
if (req.method !== 'POST') {
8+
return res.status(405).json({ error: 'Method not allowed' });
9+
}
10+
11+
try {
12+
const { dataUrl, apiKey, email } = req.body;
13+
14+
if (!dataUrl || !dataUrl.startsWith('data:')) {
15+
return res.status(400).json({ error: 'Invalid data URL' });
16+
}
17+
18+
const redis = getRedisClient();
19+
20+
// Verify API key or email
21+
let validUser = null;
22+
23+
if (apiKey) {
24+
// Check if API key is valid
25+
const keyData = await redis.get(`api_key:${apiKey}`);
26+
if (keyData) {
27+
validUser = JSON.parse(keyData).email;
28+
}
29+
} else if (email) {
30+
// Check if email has an API key
31+
const existingKey = await redis.get(`api_key:email:${email}`);
32+
if (existingKey) {
33+
validUser = email;
34+
}
35+
}
36+
37+
if (!validUser) {
38+
return res.status(403).json({
39+
error: 'Payment required',
40+
requiresPayment: true,
41+
message: 'Please purchase lifetime access to create permanent short URLs'
42+
});
43+
}
44+
45+
// Generate a unique short ID
46+
let shortId;
47+
let exists = true;
48+
49+
while (exists) {
50+
shortId = nanoid();
51+
exists = await redis.exists(`url:${shortId}`);
52+
}
53+
54+
// Store the mapping permanently (no expiration)
55+
await redis.set(`url:${shortId}`, dataUrl);
56+
await redis.set(`url:${shortId}:owner`, validUser);
57+
58+
// Return the short URL
59+
const shortUrl = `${process.env.VERCEL_URL || 'https://attest.ink'}/s/${shortId}`;
60+
61+
res.status(200).json({ shortUrl, shortId });
62+
} catch (error) {
63+
console.error('Error shortening URL:', error);
64+
res.status(500).json({ error: 'Failed to shorten URL' });
65+
}
66+
}

api/verify-payment.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import Stripe from 'stripe';
2+
import { getRedisClient } from './lib/redis.js';
3+
import { customAlphabet } from 'nanoid';
4+
5+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
6+
const generateApiKey = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 32);
7+
8+
export default async function handler(req, res) {
9+
if (req.method !== 'POST') {
10+
return res.status(405).json({ error: 'Method not allowed' });
11+
}
12+
13+
try {
14+
const { sessionId } = req.body;
15+
16+
if (!sessionId) {
17+
return res.status(400).json({ error: 'Session ID is required' });
18+
}
19+
20+
// Retrieve the session from Stripe
21+
const session = await stripe.checkout.sessions.retrieve(sessionId);
22+
23+
if (session.payment_status !== 'paid') {
24+
return res.status(400).json({ error: 'Payment not completed' });
25+
}
26+
27+
const email = session.customer_email || session.customer_details?.email;
28+
if (!email) {
29+
return res.status(400).json({ error: 'No email found in session' });
30+
}
31+
32+
const redis = getRedisClient();
33+
34+
// Check if API key already exists for this email
35+
let apiKey = await redis.get(`api_key:email:${email}`);
36+
37+
if (!apiKey) {
38+
// Generate new API key
39+
apiKey = `ak_${generateApiKey()}`;
40+
41+
// Store the API key mappings (never expire)
42+
await redis.set(`api_key:email:${email}`, apiKey);
43+
await redis.set(`api_key:${apiKey}`, JSON.stringify({
44+
email,
45+
created: new Date().toISOString(),
46+
stripeSessionId: sessionId
47+
}));
48+
}
49+
50+
// If there was attestation data in the metadata, create the short URL
51+
let shortUrl = null;
52+
if (session.metadata?.attestationData) {
53+
try {
54+
const attestationData = JSON.parse(session.metadata.attestationData);
55+
const dataUrl = `data:application/json;base64,${btoa(JSON.stringify(attestationData))}`;
56+
57+
// Generate short ID
58+
const shortIdGenerator = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 8);
59+
let shortId;
60+
let exists = true;
61+
62+
while (exists) {
63+
shortId = shortIdGenerator();
64+
exists = await redis.exists(`url:${shortId}`);
65+
}
66+
67+
// Store permanently (no expiration)
68+
await redis.set(`url:${shortId}`, dataUrl);
69+
await redis.set(`url:${shortId}:owner`, email);
70+
71+
shortUrl = `${process.env.VERCEL_URL || 'https://attest.ink'}/s/${shortId}`;
72+
} catch (error) {
73+
console.error('Error creating short URL from attestation data:', error);
74+
}
75+
}
76+
77+
res.status(200).json({
78+
success: true,
79+
apiKey,
80+
email,
81+
shortUrl
82+
});
83+
} catch (error) {
84+
console.error('Error verifying payment:', error);
85+
res.status(500).json({ error: 'Failed to verify payment' });
86+
}
87+
}

0 commit comments

Comments
 (0)