Skip to content
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ _This replaces the legacy [url-shortening-api-netlify-supabase](https://github.c
- **URL Shortening**: Convert long URLs into short, manageable links that are easier to share.
- **URL Validation**: Ensures that only valid URLs with proper protocols are processed.
- **URL Redirection**: Redirect users to the original long URL based on the short URL.
- **Link Tracking**: Track which short URLs are served.
- **Retrieve Latest Shortened URLs**: Access the most recently created short URLs.
- **URL Count**: Get the total number of URLs shortened.
- **API Versioning**: Retrieve the current version of the API.
Expand Down
1 change: 0 additions & 1 deletion netlify/edge-functions/latest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const { fetchFromSupabase } = handler();
* curl -X GET "https://your-api-url/latest?count=5"
*/
export default async (request: Request): Promise<Response> => {
console.log("here");
try {
const url = new URL(request.url);
const count = url.searchParams.get("count") || "10";
Expand Down
78 changes: 68 additions & 10 deletions netlify/edge-functions/redirect.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import handler from "./utils.ts";
const { fetchFromSupabase } = handler();
const { fetchFromSupabase, logClick, validateIpAddress } = handler();

const trackClicks = Deno.env.get("URLSHORT_TRACK_CLICKS") || false;
/**
* Redirects to the long URL associated with the given short URL.
* Handles redirection for shortened URLs, logs the click, IP address, and hostname.
*
* @param request - The incoming HTTP request.
* @returns A redirection response to the long URL or an error message.
* @throws If an error occurs while fetching the long URL from Supabase.
* @param request - The incoming request object.
* @returns A response object with a redirection or an error message.
* @throws Throws an error if there is an issue with fetching data from Supabase or logging the click.
*
* @example
* // Example usage
* curl -X GET https://your-api-url/shortUrl
*/
export default async (request: Request): Promise<Response> => {
export default async (
request: Request,
connInfo: Deno.ServeHandlerInfo
): Promise<Response> => {
try {
const shortUrl = new URL(request.url).pathname.replace("/", "");

const data = await fetchFromSupabase(
`urls?select=long_url&short_url=eq.${shortUrl}`,
`urls?select=id,long_url&short_url=eq.${shortUrl}`,
{ method: "GET" }
);

Expand All @@ -27,9 +30,34 @@ export default async (request: Request): Promise<Response> => {
});
}

const urlId = data[0].id;
const longUrl = data[0].long_url;

if (trackClicks) {
// Extract IP address from connInfo
const addr = connInfo.remoteAddr as Deno.NetAddr;
const ip = addr?.hostname || "";
let ipAddress = ip || request.headers.get("x-forwarded-for") || "";

let hostname = "";
if (validateIpAddress(ipAddress)) {
try {
hostname = await getHostnameFromIp(ipAddress);
} catch (error) {
console.error(`Error resolving hostname for IP ${ipAddress}:`, error);
}
} else {
ipAddress = "";
hostname = "unknown";
}

// Log the click with IP address and hostname
await logClick(urlId, ipAddress, hostname);
}

return new Response(null, {
status: 301,
headers: { Location: data[0].long_url },
status: 302,
headers: { Location: longUrl },
});
} catch (error) {
console.error("Error:", error);
Expand All @@ -42,3 +70,33 @@ export default async (request: Request): Promise<Response> => {
);
}
};

/**
* Resolves the hostname from an IP address using a reverse DNS lookup.
*
* @param ip - The IP address to resolve.
* @returns A promise that resolves to the hostname.
* @throws Throws an error if the DNS lookup fails.
*
* @example
* // How to use the function
* const hostname = await getHostnameFromIp("8.8.8.8");
* console.log(hostname); // Example output: "dns.google"
*/
export async function getHostnameFromIp(ip: string): Promise<string> {
try {
// Perform a reverse DNS lookup to get the PTR record for the IP address
const hostnames = await Deno.resolveDns(ip, "PTR");

if (hostnames.length > 0) {
// Return the first hostname in the result
return hostnames[0];
} else {
//throw new Error(`No hostname found for IP: ${ip}`);
console.error(`No hostname found for IP: ${ip}`);
}
} catch (error) {
console.error(`Failed to resolve hostname for IP: ${ip}`, error);
throw error;
}
}
3 changes: 2 additions & 1 deletion netlify/edge-functions/shorten.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { isURL } from "https://deno.land/x/is_url/mod.ts";

import { headers } from "./headers.ts";
import handler from "./utils.ts";
const urlBase = Deno.env.get("URL_BASE") || "";
const { generateShortUrl } = handler();

const urlBase = Deno.env.get("URLSHORT_URL_BASE") || "";

/**
* Shortens a given long URL and stores it in Supabase.
*
Expand Down
Loading