From ec6beec87b29de9a40c951c7de4e20c4ab47a987 Mon Sep 17 00:00:00 2001 From: ezkeel-admin Date: Thu, 11 Jun 2026 18:49:06 +0000 Subject: [PATCH] Initial template --- Dockerfile | 12 ++++++ README.md | 9 +++++ package.json | 12 ++++++ server.js | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 package.json create mode 100644 server.js diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..56e8168 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm install + +COPY server.js ./ + +EXPOSE 3000 + +CMD ["npm", "start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..130d0d8 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# url-shortener + +Express + PostgreSQL link shortener. + +- `POST /api/links` with `{"url": "https://example.com"}` returns a 7-char hash and short URL +- `GET /` 302-redirects to the original URL and counts the hit +- `GET /api/links` lists recent links with hit counts + +Reads `DATABASE_URL` and `PORT` from the environment — EZKeel provides both at deploy time. diff --git a/package.json b/package.json new file mode 100644 index 0000000..a7dcd43 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "url-shortener", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "pg": "^8.11.3" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..ae9b655 --- /dev/null +++ b/server.js @@ -0,0 +1,103 @@ +const express = require('express'); +const crypto = require('crypto'); +const { Pool } = require('pg'); + +const app = express(); +app.use(express.json()); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +async function initDB() { + const client = await pool.connect(); + try { + await client.query(` + CREATE TABLE IF NOT EXISTS links ( + hash TEXT PRIMARY KEY, + url TEXT NOT NULL, + hits INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT NOW() + ) + `); + console.log('Database table ready'); + } finally { + client.release(); + } +} + +function makeHash(url) { + return crypto.createHash('sha256').update(url).digest('base64url').slice(0, 7); +} + +// POST /api/links {"url": "https://example.com"} -> {"hash": "...", "short_url": "..."} +app.post('/api/links', async (req, res) => { + const { url } = req.body; + let parsed; + try { + parsed = new URL(url); + } catch { + return res.status(400).json({ error: '"url" must be a valid URL' }); + } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return res.status(400).json({ error: 'only http/https URLs allowed' }); + } + const hash = makeHash(parsed.href); + try { + await pool.query( + 'INSERT INTO links (hash, url) VALUES ($1, $2) ON CONFLICT (hash) DO NOTHING', + [hash, parsed.href] + ); + res.status(201).json({ + hash, + short_url: `${req.protocol}://${req.get('host')}/${hash}`, + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Failed to create link' }); + } +}); + +// GET /api/links -> recent links with hit counts +app.get('/api/links', async (req, res) => { + try { + const { rows } = await pool.query( + 'SELECT hash, url, hits, created_at FROM links ORDER BY created_at DESC LIMIT 50' + ); + res.json(rows); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Failed to list links' }); + } +}); + +// GET /:hash -> 302 redirect +app.get('/:hash([A-Za-z0-9_-]{7})', async (req, res) => { + try { + const { rows } = await pool.query( + 'UPDATE links SET hits = hits + 1 WHERE hash = $1 RETURNING url', + [req.params.hash] + ); + if (rows.length === 0) return res.status(404).json({ error: 'Link not found' }); + res.redirect(302, rows[0].url); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Failed to resolve link' }); + } +}); + +app.get('/', (req, res) => { + res.json({ + name: 'url-shortener', + usage: { + shorten: 'POST /api/links {"url": "https://example.com"}', + list: 'GET /api/links', + follow: 'GET /', + }, + }); +}); + +initDB().then(() => { + const PORT = process.env.PORT || 3000; + app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); +});