@linkforty/core 1.0.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -43
- package/dist/index.js.map +1 -1
- package/dist/lib/database.d.ts.map +1 -1
- package/dist/lib/database.js +160 -12
- package/dist/lib/database.js.map +1 -1
- package/dist/lib/event-emitter.d.ts +46 -0
- package/dist/lib/event-emitter.d.ts.map +1 -0
- package/dist/lib/event-emitter.js +24 -0
- package/dist/lib/event-emitter.js.map +1 -0
- package/dist/lib/fingerprint.d.ts +64 -0
- package/dist/lib/fingerprint.d.ts.map +1 -0
- package/dist/lib/fingerprint.js +343 -0
- package/dist/lib/fingerprint.js.map +1 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +12 -21
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/webhook.d.ts +18 -0
- package/dist/lib/webhook.d.ts.map +1 -0
- package/dist/lib/webhook.js +141 -0
- package/dist/lib/webhook.js.map +1 -0
- package/dist/routes/analytics.js +14 -17
- package/dist/routes/analytics.js.map +1 -1
- package/dist/routes/debug.d.ts +7 -0
- package/dist/routes/debug.d.ts.map +1 -0
- package/dist/routes/debug.js +318 -0
- package/dist/routes/debug.js.map +1 -0
- package/dist/routes/index.d.ts +5 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +8 -9
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/links.d.ts.map +1 -1
- package/dist/routes/links.js +53 -38
- package/dist/routes/links.js.map +1 -1
- package/dist/routes/preview.d.ts +3 -0
- package/dist/routes/preview.d.ts.map +1 -0
- package/dist/routes/preview.js +222 -0
- package/dist/routes/preview.js.map +1 -0
- package/dist/routes/qr.d.ts +6 -0
- package/dist/routes/qr.d.ts.map +1 -0
- package/dist/routes/qr.js +130 -0
- package/dist/routes/qr.js.map +1 -0
- package/dist/routes/redirect.d.ts.map +1 -1
- package/dist/routes/redirect.js +142 -22
- package/dist/routes/redirect.js.map +1 -1
- package/dist/routes/sdk.d.ts +7 -0
- package/dist/routes/sdk.d.ts.map +1 -0
- package/dist/routes/sdk.js +262 -0
- package/dist/routes/sdk.js.map +1 -0
- package/dist/routes/webhooks.d.ts +3 -0
- package/dist/routes/webhooks.d.ts.map +1 -0
- package/dist/routes/webhooks.js +176 -0
- package/dist/routes/webhooks.js.map +1 -0
- package/dist/scripts/migrate.js +2 -4
- package/dist/scripts/migrate.js.map +1 -1
- package/dist/types/index.d.ts +81 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -2
- package/package.json +11 -7
package/dist/routes/links.js
CHANGED
|
@@ -1,35 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
term: zod_1.z.string().optional(),
|
|
20
|
-
content: zod_1.z.string().optional(),
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { db } from '../lib/database.js';
|
|
3
|
+
import { generateShortCode } from '../lib/utils.js';
|
|
4
|
+
const createLinkSchema = z.object({
|
|
5
|
+
userId: z.string().uuid(),
|
|
6
|
+
originalUrl: z.string().url(),
|
|
7
|
+
title: z.string().optional(),
|
|
8
|
+
description: z.string().optional(),
|
|
9
|
+
iosUrl: z.string().url().optional(),
|
|
10
|
+
androidUrl: z.string().url().optional(),
|
|
11
|
+
webFallbackUrl: z.string().url().optional(),
|
|
12
|
+
customCode: z.string().optional(),
|
|
13
|
+
utmParameters: z.object({
|
|
14
|
+
source: z.string().optional(),
|
|
15
|
+
medium: z.string().optional(),
|
|
16
|
+
campaign: z.string().optional(),
|
|
17
|
+
term: z.string().optional(),
|
|
18
|
+
content: z.string().optional(),
|
|
21
19
|
}).optional(),
|
|
22
|
-
targetingRules:
|
|
23
|
-
countries:
|
|
24
|
-
devices:
|
|
25
|
-
languages:
|
|
20
|
+
targetingRules: z.object({
|
|
21
|
+
countries: z.array(z.string()).optional(),
|
|
22
|
+
devices: z.array(z.enum(['ios', 'android', 'web'])).optional(),
|
|
23
|
+
languages: z.array(z.string()).optional(),
|
|
26
24
|
}).optional(),
|
|
27
|
-
|
|
25
|
+
ogTitle: z.string().optional(),
|
|
26
|
+
ogDescription: z.string().optional(),
|
|
27
|
+
ogImageUrl: z.string().url().optional(),
|
|
28
|
+
ogType: z.string().optional(),
|
|
29
|
+
attributionWindowHours: z.number()
|
|
30
|
+
.int('Attribution window must be an integer')
|
|
31
|
+
.min(1, 'Attribution window must be at least 1 hour')
|
|
32
|
+
.max(2160, 'Attribution window must be at most 2160 hours (90 days)')
|
|
33
|
+
.optional(),
|
|
34
|
+
expiresAt: z.string().datetime().optional(),
|
|
28
35
|
});
|
|
29
36
|
const updateLinkSchema = createLinkSchema.partial().extend({
|
|
30
|
-
isActive:
|
|
37
|
+
isActive: z.boolean().optional(),
|
|
31
38
|
}).omit({ userId: true });
|
|
32
|
-
async function linkRoutes(fastify) {
|
|
39
|
+
export async function linkRoutes(fastify) {
|
|
33
40
|
// Get all links for a user
|
|
34
41
|
fastify.get('/api/links', async (request) => {
|
|
35
42
|
const { userId } = request.query;
|
|
@@ -45,7 +52,7 @@ async function linkRoutes(fastify) {
|
|
|
45
52
|
GROUP BY l.id
|
|
46
53
|
ORDER BY l.created_at DESC
|
|
47
54
|
`;
|
|
48
|
-
const result = await
|
|
55
|
+
const result = await db.query(query, [userId]);
|
|
49
56
|
return result.rows.map(row => ({
|
|
50
57
|
...row,
|
|
51
58
|
clickCount: parseInt(row.click_count),
|
|
@@ -60,7 +67,7 @@ async function linkRoutes(fastify) {
|
|
|
60
67
|
if (!userId) {
|
|
61
68
|
throw new Error('userId query parameter is required');
|
|
62
69
|
}
|
|
63
|
-
const result = await
|
|
70
|
+
const result = await db.query(`SELECT l.*, COUNT(ce.id) as click_count
|
|
64
71
|
FROM links l
|
|
65
72
|
LEFT JOIN click_events ce ON l.id = ce.link_id
|
|
66
73
|
WHERE l.id = $1 AND l.user_id = $2
|
|
@@ -80,34 +87,42 @@ async function linkRoutes(fastify) {
|
|
|
80
87
|
fastify.post('/api/links', async (request) => {
|
|
81
88
|
const data = createLinkSchema.parse(request.body);
|
|
82
89
|
// Generate short code
|
|
83
|
-
let shortCode = data.customCode ||
|
|
90
|
+
let shortCode = data.customCode || generateShortCode();
|
|
84
91
|
// Ensure short code is unique
|
|
85
92
|
let attempts = 0;
|
|
86
93
|
while (attempts < 10) {
|
|
87
|
-
const existing = await
|
|
94
|
+
const existing = await db.query('SELECT id FROM links WHERE short_code = $1', [shortCode]);
|
|
88
95
|
if (existing.rows.length === 0) {
|
|
89
96
|
break;
|
|
90
97
|
}
|
|
91
|
-
shortCode =
|
|
98
|
+
shortCode = generateShortCode();
|
|
92
99
|
attempts++;
|
|
93
100
|
}
|
|
94
101
|
if (attempts >= 10) {
|
|
95
102
|
throw new Error('Unable to generate unique short code');
|
|
96
103
|
}
|
|
97
|
-
const result = await
|
|
98
|
-
user_id, short_code, original_url, title,
|
|
99
|
-
ios_url, android_url, web_fallback_url, utm_parameters, targeting_rules,
|
|
100
|
-
|
|
104
|
+
const result = await db.query(`INSERT INTO links (
|
|
105
|
+
user_id, short_code, original_url, title, description,
|
|
106
|
+
ios_url, android_url, web_fallback_url, utm_parameters, targeting_rules,
|
|
107
|
+
og_title, og_description, og_image_url, og_type,
|
|
108
|
+
attribution_window_hours, expires_at
|
|
109
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
|
101
110
|
RETURNING *`, [
|
|
102
111
|
data.userId,
|
|
103
112
|
shortCode,
|
|
104
113
|
data.originalUrl,
|
|
105
114
|
data.title || null,
|
|
115
|
+
data.description || null,
|
|
106
116
|
data.iosUrl || null,
|
|
107
117
|
data.androidUrl || null,
|
|
108
118
|
data.webFallbackUrl || null,
|
|
109
119
|
JSON.stringify(data.utmParameters || {}),
|
|
110
120
|
JSON.stringify(data.targetingRules || {}),
|
|
121
|
+
data.ogTitle || null,
|
|
122
|
+
data.ogDescription || null,
|
|
123
|
+
data.ogImageUrl || null,
|
|
124
|
+
data.ogType || 'website',
|
|
125
|
+
data.attributionWindowHours || 168, // Default 7 days
|
|
111
126
|
data.expiresAt || null,
|
|
112
127
|
]);
|
|
113
128
|
const link = result.rows[0];
|
|
@@ -149,7 +164,7 @@ async function linkRoutes(fastify) {
|
|
|
149
164
|
}
|
|
150
165
|
updates.push('updated_at = NOW()');
|
|
151
166
|
values.push(id, userId);
|
|
152
|
-
const result = await
|
|
167
|
+
const result = await db.query(`UPDATE links SET ${updates.join(', ')}
|
|
153
168
|
WHERE id = $${paramIndex} AND user_id = $${paramIndex + 1}
|
|
154
169
|
RETURNING *`, values);
|
|
155
170
|
if (result.rows.length === 0) {
|
|
@@ -169,7 +184,7 @@ async function linkRoutes(fastify) {
|
|
|
169
184
|
if (!userId) {
|
|
170
185
|
throw new Error('userId query parameter is required');
|
|
171
186
|
}
|
|
172
|
-
const result = await
|
|
187
|
+
const result = await db.query('DELETE FROM links WHERE id = $1 AND user_id = $2 RETURNING id', [id, userId]);
|
|
173
188
|
if (result.rows.length === 0) {
|
|
174
189
|
throw new Error('Link not found');
|
|
175
190
|
}
|
package/dist/routes/links.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/routes/links.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/routes/links.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/routes/preview.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AA2J1C,wBAAsB,aAAa,CAAC,OAAO,EAAE,eAAe,iBAgG3D"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { db } from '../lib/database.js';
|
|
2
|
+
/**
|
|
3
|
+
* Detect if the request is from a social media scraper/bot
|
|
4
|
+
* These bots crawl links to generate previews when shared on social platforms
|
|
5
|
+
*/
|
|
6
|
+
function isSocialScraper(userAgent) {
|
|
7
|
+
const scraperPatterns = [
|
|
8
|
+
/facebookexternalhit/i, // Facebook
|
|
9
|
+
/Facebot/i, // Facebook
|
|
10
|
+
/Twitterbot/i, // Twitter
|
|
11
|
+
/LinkedInBot/i, // LinkedIn
|
|
12
|
+
/Slackbot/i, // Slack
|
|
13
|
+
/Discordbot/i, // Discord
|
|
14
|
+
/TelegramBot/i, // Telegram
|
|
15
|
+
/WhatsApp/i, // WhatsApp
|
|
16
|
+
/PinterestBot/i, // Pinterest
|
|
17
|
+
/SkypeUriPreview/i, // Skype
|
|
18
|
+
/Googlebot/i, // Google (for search previews)
|
|
19
|
+
/bingbot/i, // Bing
|
|
20
|
+
/ia_archiver/i, // Alexa
|
|
21
|
+
];
|
|
22
|
+
return scraperPatterns.some(pattern => pattern.test(userAgent));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generate HTML preview page with Open Graph meta tags
|
|
26
|
+
*/
|
|
27
|
+
function generatePreviewHTML(link, shortUrl, autoRedirect = true) {
|
|
28
|
+
// Use OG-specific values if provided, otherwise fall back to regular title/description
|
|
29
|
+
const ogTitle = link.og_title || link.title || 'Shared Link';
|
|
30
|
+
const ogDescription = link.og_description || link.description || '';
|
|
31
|
+
const ogImage = link.og_image_url || '';
|
|
32
|
+
const ogType = link.og_type || 'website';
|
|
33
|
+
const ogUrl = shortUrl;
|
|
34
|
+
// Auto-redirect after 2 seconds for human visitors
|
|
35
|
+
const metaRefresh = autoRedirect
|
|
36
|
+
? `<meta http-equiv="refresh" content="2;url=${link.original_url}">`
|
|
37
|
+
: '';
|
|
38
|
+
return `<!DOCTYPE html>
|
|
39
|
+
<html lang="en">
|
|
40
|
+
<head>
|
|
41
|
+
<meta charset="UTF-8">
|
|
42
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
43
|
+
<title>${escapeHtml(ogTitle)}</title>
|
|
44
|
+
|
|
45
|
+
<!-- Open Graph / Facebook -->
|
|
46
|
+
<meta property="og:type" content="${escapeHtml(ogType)}">
|
|
47
|
+
<meta property="og:url" content="${escapeHtml(ogUrl)}">
|
|
48
|
+
<meta property="og:title" content="${escapeHtml(ogTitle)}">
|
|
49
|
+
<meta property="og:description" content="${escapeHtml(ogDescription)}">
|
|
50
|
+
${ogImage ? `<meta property="og:image" content="${escapeHtml(ogImage)}">` : ''}
|
|
51
|
+
${ogImage ? `<meta property="og:image:secure_url" content="${escapeHtml(ogImage)}">` : ''}
|
|
52
|
+
|
|
53
|
+
<!-- Twitter -->
|
|
54
|
+
<meta name="twitter:card" content="${ogImage ? 'summary_large_image' : 'summary'}">
|
|
55
|
+
<meta name="twitter:url" content="${escapeHtml(ogUrl)}">
|
|
56
|
+
<meta name="twitter:title" content="${escapeHtml(ogTitle)}">
|
|
57
|
+
<meta name="twitter:description" content="${escapeHtml(ogDescription)}">
|
|
58
|
+
${ogImage ? `<meta name="twitter:image" content="${escapeHtml(ogImage)}">` : ''}
|
|
59
|
+
|
|
60
|
+
<!-- LinkedIn -->
|
|
61
|
+
<meta property="og:site_name" content="LinkForty">
|
|
62
|
+
|
|
63
|
+
${metaRefresh}
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
body {
|
|
67
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
min-height: 100vh;
|
|
72
|
+
margin: 0;
|
|
73
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
74
|
+
color: white;
|
|
75
|
+
text-align: center;
|
|
76
|
+
padding: 20px;
|
|
77
|
+
}
|
|
78
|
+
.container {
|
|
79
|
+
max-width: 600px;
|
|
80
|
+
}
|
|
81
|
+
h1 {
|
|
82
|
+
font-size: 2.5rem;
|
|
83
|
+
margin-bottom: 1rem;
|
|
84
|
+
}
|
|
85
|
+
p {
|
|
86
|
+
font-size: 1.25rem;
|
|
87
|
+
margin-bottom: 2rem;
|
|
88
|
+
opacity: 0.9;
|
|
89
|
+
}
|
|
90
|
+
.link {
|
|
91
|
+
display: inline-block;
|
|
92
|
+
padding: 12px 24px;
|
|
93
|
+
background: white;
|
|
94
|
+
color: #667eea;
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
border-radius: 8px;
|
|
97
|
+
font-weight: 600;
|
|
98
|
+
transition: transform 0.2s;
|
|
99
|
+
}
|
|
100
|
+
.link:hover {
|
|
101
|
+
transform: translateY(-2px);
|
|
102
|
+
}
|
|
103
|
+
.loader {
|
|
104
|
+
margin: 2rem auto;
|
|
105
|
+
border: 4px solid rgba(255, 255, 255, 0.3);
|
|
106
|
+
border-top: 4px solid white;
|
|
107
|
+
border-radius: 50%;
|
|
108
|
+
width: 40px;
|
|
109
|
+
height: 40px;
|
|
110
|
+
animation: spin 1s linear infinite;
|
|
111
|
+
}
|
|
112
|
+
@keyframes spin {
|
|
113
|
+
0% { transform: rotate(0deg); }
|
|
114
|
+
100% { transform: rotate(360deg); }
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
117
|
+
</head>
|
|
118
|
+
<body>
|
|
119
|
+
<div class="container">
|
|
120
|
+
${ogImage ? `<img src="${escapeHtml(ogImage)}" alt="${escapeHtml(ogTitle)}" style="max-width: 100%; border-radius: 12px; margin-bottom: 2rem; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">` : ''}
|
|
121
|
+
<h1>${escapeHtml(ogTitle)}</h1>
|
|
122
|
+
${ogDescription ? `<p>${escapeHtml(ogDescription)}</p>` : ''}
|
|
123
|
+
${autoRedirect ? '<div class="loader"></div><p>Redirecting you...</p>' : ''}
|
|
124
|
+
<a href="${escapeHtml(link.original_url)}" class="link">
|
|
125
|
+
${autoRedirect ? 'Click here if not redirected' : 'Continue to destination'}
|
|
126
|
+
</a>
|
|
127
|
+
</div>
|
|
128
|
+
</body>
|
|
129
|
+
</html>`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Escape HTML to prevent XSS
|
|
133
|
+
*/
|
|
134
|
+
function escapeHtml(text) {
|
|
135
|
+
const map = {
|
|
136
|
+
'&': '&',
|
|
137
|
+
'<': '<',
|
|
138
|
+
'>': '>',
|
|
139
|
+
'"': '"',
|
|
140
|
+
"'": ''',
|
|
141
|
+
};
|
|
142
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
143
|
+
}
|
|
144
|
+
export async function previewRoutes(fastify) {
|
|
145
|
+
/**
|
|
146
|
+
* GET /:shortCode/preview
|
|
147
|
+
* Always return HTML preview page with Open Graph tags
|
|
148
|
+
* Auto-redirects after 2 seconds for human visitors
|
|
149
|
+
*/
|
|
150
|
+
fastify.get('/:shortCode/preview', async (request, reply) => {
|
|
151
|
+
const { shortCode } = request.params;
|
|
152
|
+
const baseUrl = request.headers.host
|
|
153
|
+
? `${request.protocol}://${request.headers.host}`
|
|
154
|
+
: 'https://link.forty';
|
|
155
|
+
try {
|
|
156
|
+
// Lookup link by short code
|
|
157
|
+
const result = await db.query(`SELECT * FROM links
|
|
158
|
+
WHERE short_code = $1 AND is_active = true
|
|
159
|
+
AND (expires_at IS NULL OR expires_at > NOW())`, [shortCode]);
|
|
160
|
+
if (result.rows.length === 0) {
|
|
161
|
+
return reply.status(404).send('Link not found');
|
|
162
|
+
}
|
|
163
|
+
const link = result.rows[0];
|
|
164
|
+
const shortUrl = `${baseUrl}/${shortCode}`;
|
|
165
|
+
// Return HTML with OG tags and auto-redirect
|
|
166
|
+
const html = generatePreviewHTML(link, shortUrl, true);
|
|
167
|
+
return reply
|
|
168
|
+
.header('Content-Type', 'text/html; charset=utf-8')
|
|
169
|
+
.send(html);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
fastify.log.error(`Error generating preview: ${error}`);
|
|
173
|
+
return reply.status(500).send('Error generating preview');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
/**
|
|
177
|
+
* Middleware for /:shortCode route to detect social scrapers
|
|
178
|
+
* If scraper detected, return OG preview instead of redirecting
|
|
179
|
+
*
|
|
180
|
+
* Note: This should be registered BEFORE the main redirect route
|
|
181
|
+
*/
|
|
182
|
+
fastify.addHook('preHandler', async (request, reply) => {
|
|
183
|
+
// Only apply to short code routes (not API routes, not preview routes)
|
|
184
|
+
const path = request.url.split('?')[0]; // Remove query string
|
|
185
|
+
if (path.startsWith('/api/') ||
|
|
186
|
+
path.endsWith('/preview') ||
|
|
187
|
+
path === '/' ||
|
|
188
|
+
path.includes('.')) {
|
|
189
|
+
return; // Skip this hook
|
|
190
|
+
}
|
|
191
|
+
const userAgent = request.headers['user-agent'] || '';
|
|
192
|
+
const shortCode = path.split('/').pop() || '';
|
|
193
|
+
const baseUrl = request.headers.host
|
|
194
|
+
? `${request.protocol}://${request.headers.host}`
|
|
195
|
+
: 'https://link.forty';
|
|
196
|
+
// If it's a social scraper, return OG preview HTML
|
|
197
|
+
if (isSocialScraper(userAgent)) {
|
|
198
|
+
try {
|
|
199
|
+
const result = await db.query(`SELECT * FROM links
|
|
200
|
+
WHERE short_code = $1 AND is_active = true
|
|
201
|
+
AND (expires_at IS NULL OR expires_at > NOW())`, [shortCode]);
|
|
202
|
+
if (result.rows.length > 0) {
|
|
203
|
+
const link = result.rows[0];
|
|
204
|
+
const shortUrl = `${baseUrl}/${shortCode}`;
|
|
205
|
+
// Return HTML with OG tags, no auto-redirect for bots
|
|
206
|
+
const html = generatePreviewHTML(link, shortUrl, false);
|
|
207
|
+
reply
|
|
208
|
+
.header('Content-Type', 'text/html; charset=utf-8')
|
|
209
|
+
.send(html);
|
|
210
|
+
// Stop the request here, don't continue to redirect route
|
|
211
|
+
return reply;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
fastify.log.error(`Error in social scraper detection: ${error}`);
|
|
216
|
+
// Continue to normal redirect route on error
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Not a social scraper, continue to normal redirect logic
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.js","sourceRoot":"","sources":["../../src/routes/preview.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qr.d.ts","sourceRoot":"","sources":["../../src/routes/qr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI1C;;GAEG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,iBAuItD"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import QRCode from 'qrcode';
|
|
2
|
+
import { db } from '../lib/database.js';
|
|
3
|
+
/**
|
|
4
|
+
* QR Code Routes - Generate QR codes for links
|
|
5
|
+
*/
|
|
6
|
+
export async function qrRoutes(fastify) {
|
|
7
|
+
/**
|
|
8
|
+
* GET /api/links/:id/qr
|
|
9
|
+
* Generate QR code for a link
|
|
10
|
+
*
|
|
11
|
+
* Query parameters:
|
|
12
|
+
* - format: 'png' | 'svg' (default: 'png')
|
|
13
|
+
* - size: number 128-2048 (default: 512)
|
|
14
|
+
* - color: hex color for foreground (default: '#000000')
|
|
15
|
+
* - bgcolor: hex color for background (default: '#ffffff')
|
|
16
|
+
*
|
|
17
|
+
* Returns: QR code image (PNG or SVG)
|
|
18
|
+
*/
|
|
19
|
+
fastify.get('/api/links/:id/qr', async (request, reply) => {
|
|
20
|
+
const { id } = request.params;
|
|
21
|
+
const query = request.query;
|
|
22
|
+
const format = (query.format || 'png');
|
|
23
|
+
const size = Math.min(Math.max(parseInt(query.size || '512', 10), 128), 2048);
|
|
24
|
+
const color = query.color || '#000000';
|
|
25
|
+
const bgcolor = query.bgcolor || '#ffffff';
|
|
26
|
+
// Validate format
|
|
27
|
+
if (!['png', 'svg'].includes(format)) {
|
|
28
|
+
return reply.status(400).send({ error: 'Invalid format. Use "png" or "svg".' });
|
|
29
|
+
}
|
|
30
|
+
// Build cache key
|
|
31
|
+
const cacheKey = `qr:${id}:${format}:${size}:${color}:${bgcolor}`;
|
|
32
|
+
// Try to get from cache
|
|
33
|
+
if (fastify.redis) {
|
|
34
|
+
try {
|
|
35
|
+
const cached = await fastify.redis.get(cacheKey);
|
|
36
|
+
if (cached) {
|
|
37
|
+
fastify.log.info(`QR code cache hit: ${cacheKey}`);
|
|
38
|
+
if (format === 'png') {
|
|
39
|
+
// Cached PNG is base64
|
|
40
|
+
const buffer = Buffer.from(cached, 'base64');
|
|
41
|
+
return reply
|
|
42
|
+
.type('image/png')
|
|
43
|
+
.header('Cache-Control', 'public, max-age=86400') // 24 hours
|
|
44
|
+
.send(buffer);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Cached SVG is text
|
|
48
|
+
return reply
|
|
49
|
+
.type('image/svg+xml')
|
|
50
|
+
.header('Cache-Control', 'public, max-age=86400')
|
|
51
|
+
.send(cached);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
fastify.log.warn('Redis QR cache lookup failed');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Get link from database
|
|
60
|
+
const result = await db.query('SELECT short_code, original_url FROM links WHERE id = $1 AND is_active = true', [id]);
|
|
61
|
+
if (result.rows.length === 0) {
|
|
62
|
+
return reply.status(404).send({ error: 'Link not found' });
|
|
63
|
+
}
|
|
64
|
+
const link = result.rows[0];
|
|
65
|
+
// Build short URL (use original_url as fallback if short_code not available)
|
|
66
|
+
// In production, you'd want to use your actual domain
|
|
67
|
+
const shortUrl = link.short_code
|
|
68
|
+
? `${request.protocol}://${request.hostname}/${link.short_code}`
|
|
69
|
+
: link.original_url;
|
|
70
|
+
try {
|
|
71
|
+
// QR code options
|
|
72
|
+
const options = {
|
|
73
|
+
errorCorrectionLevel: 'M', // Medium error correction
|
|
74
|
+
margin: 1, // Quiet zone margin
|
|
75
|
+
width: size,
|
|
76
|
+
color: {
|
|
77
|
+
dark: color,
|
|
78
|
+
light: bgcolor,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
if (format === 'png') {
|
|
82
|
+
// Generate PNG as buffer
|
|
83
|
+
const buffer = await QRCode.toBuffer(shortUrl, options);
|
|
84
|
+
// Cache as base64
|
|
85
|
+
if (fastify.redis) {
|
|
86
|
+
try {
|
|
87
|
+
await fastify.redis.setex(cacheKey, 86400, buffer.toString('base64')); // 24 hour TTL
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
fastify.log.warn('Failed to cache QR code');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return reply
|
|
94
|
+
.type('image/png')
|
|
95
|
+
.header('Cache-Control', 'public, max-age=86400')
|
|
96
|
+
.header('Content-Disposition', `inline; filename="qr-${link.short_code || 'code'}.png"`)
|
|
97
|
+
.send(buffer);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Generate SVG as string
|
|
101
|
+
const svg = await QRCode.toString(shortUrl, {
|
|
102
|
+
...options,
|
|
103
|
+
type: 'svg',
|
|
104
|
+
});
|
|
105
|
+
// Cache SVG text
|
|
106
|
+
if (fastify.redis) {
|
|
107
|
+
try {
|
|
108
|
+
await fastify.redis.setex(cacheKey, 86400, svg);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
fastify.log.warn('Failed to cache QR code');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return reply
|
|
115
|
+
.type('image/svg+xml')
|
|
116
|
+
.header('Cache-Control', 'public, max-age=86400')
|
|
117
|
+
.header('Content-Disposition', `inline; filename="qr-${link.short_code || 'code'}.svg"`)
|
|
118
|
+
.send(svg);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
fastify.log.error(`QR code generation failed: ${error.message}`);
|
|
123
|
+
return reply.status(500).send({
|
|
124
|
+
error: 'Failed to generate QR code',
|
|
125
|
+
message: error.message
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=qr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/routes/qr.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/routes/redirect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/routes/redirect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAM1C,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,iBAsS5D"}
|