Skip to content

Commit

Permalink
feat: anonymous shortening
Browse files Browse the repository at this point in the history
  • Loading branch information
cvyl committed Jun 9, 2024
1 parent ca2eef0 commit 3cb1aca
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 6 deletions.
46 changes: 41 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@clerk/themes": "^2.1.9",
"@prisma/client": "^5.15.0",
"file-saver": "^2.0.5",
"nanoid": "^5.0.7",
"next": "14.2.3",
"qrcode.react": "^3.1.0",
"react": "^18",
Expand Down
28 changes: 28 additions & 0 deletions src/app/api/metadata/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';

export async function GET(req: NextRequest) {
try {
const url = new URL(req.url);
const alias = url.searchParams.get('q');

if (!alias) {
return NextResponse.json({ error: 'Alias parameter is required' }, { status: 400 });
}

// Find the short URL using the alias
const short = await prisma.shortURL.findUnique({
where: { alias },
});

if (!short) {
return NextResponse.json({ error: 'Not Found' }, { status: 404 });
}

// Redirect to the long URL
return NextResponse.json(short.longURL);
} catch (error) {
console.error(error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
56 changes: 56 additions & 0 deletions src/app/api/random/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import prisma from '@/lib/prisma';
import anonUrl from '@/schemas/urlNonAlias';
import { nanoid } from 'nanoid';

const getIpAddress = (req: NextRequest): string | null => {
return req.headers.get('x-real-ip') || req.headers.get('x-forwarded-for') || req.ip || null;
};

export async function POST(req: NextRequest) {
try {
const { longURL } = await req.json();
const alias = nanoid(7);

// Validate the URL
const result = anonUrl.safeParse({ url: longURL });
if (!result.success) {
return NextResponse.json({ error: result.error.errors }, { status: 400 });
}

// create random user in the database
const user = await prisma.user.upsert({
where: {
clerkId: 'anon_' + getIpAddress(req)?.toString(),
},
update: {},
create: {
clerkId: 'anon_' + getIpAddress(req)?.toString() ?? 'anon',
email: 'anon_' + getIpAddress(req)?.toString() ?? 'anon',
ipAddress: getIpAddress(req),
},
});


// Check if the alias is already taken
const existingShort = await prisma.shortURL.findUnique({ where: { alias } });
if (existingShort) {
return NextResponse.json({ error: 'Alias already taken' }, { status: 400 });
}

// Create the short URL
const short = await prisma.shortURL.create({
data: {
longURL,
alias,
userId: user.id,
},
});

return NextResponse.json(short, { status: 201 });
} catch (error) {
return NextResponse.json({ error: error + 'Internal Server Error' }, { status: 500 });
}
}

91 changes: 91 additions & 0 deletions src/app/testing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use client';
import '../globals.css';
import React, { useEffect } from 'react';

export default function TestPage() {
const [longURL, setLongURL] = React.useState('');
const [shortURL, setShortURL] = React.useState('');
const [error, setError] = React.useState('');
const [alias, setAlias] = React.useState('');

async function shorten() {
const response = await fetch('/api/random', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
longURL,
}),
});

const result = await response.json();

if (response.ok) {
setShortURL(result.shortURL);
localStorage.setItem('shortURL', result.shortURL);
setError('');
} else {
setShortURL('');
setError(result.error);
localStorage.removeItem('shortURL');
}
}

async function getDestination() {
// fethc using the random api but add the alias to the url after the '?'
//we cant use json here because we are not sending any data
const response = await fetch(`/api/metadata?q=${alias}`, {
method: 'GET',
});

const result = await response.json();

if (response.ok) {
setShortURL(result.shortURL);
localStorage.setItem('shortURL', result.shortURL);
setError('');
} else {
setShortURL('');
setError(result.error);
localStorage.removeItem('shortURL');
}
}

return (
<div className="container mx-auto py-8">
<div className="max-w-lg mx-auto bg-white p-8 rounded shadow-md">
<h1 className="text-2xl font-bold mb-4">Create a Short URL</h1>
<input
type="text"
placeholder="Long URL"
value={longURL}
onChange={(e) => setLongURL(e.target.value)}
className="w-full px-3 py-2 mb-4 border border-gray-300 rounded text-black"
/>
<button
onClick={shorten}
className="w-full px-3 py-2 mb-4 bg-blue-500 text-white rounded"
>
Shorten
</button>
</div>
<div className="max-w-lg mx-auto bg-white p-8 rounded shadow-md">
<h1 className="text-2xl font-bold mb-4">Get Destination of Short URL</h1>
<input
type="text"
placeholder="Short URL"
value={alias}
onChange={(e) => setAlias(e.target.value)}
className="w-full px-3 py-2 mb-4 border border-gray-300 rounded text-black"
/>
<button
onClick={getDestination}
className="w-full px-3 py-2 mb-4 bg-blue-500 text-white rounded"
>
Get Destination
</button>
</div>
</div>
);
}
7 changes: 7 additions & 0 deletions src/schemas/urlNonAlias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';

const anonUrl = z.object({
url: z.string().url('URL must be a valid URL'),
});

export default anonUrl;
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
}

0 comments on commit 3cb1aca

Please sign in to comment.