103 lines
2.7 KiB
JavaScript
103 lines
2.7 KiB
JavaScript
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 /<hash>',
|
|
},
|
|
});
|
|
});
|
|
|
|
initDB().then(() => {
|
|
const PORT = process.env.PORT || 3000;
|
|
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
|
});
|