diff --git a/.gitignore b/.gitignore index 0e9d56c..72bfe70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .vscode -config.js node_modules +.env +error.log +info.log \ No newline at end of file diff --git a/README.md b/README.md index e17ccf2..c603aaa 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ The 1pt.co API is public so anyone can create a shortened URL +## Instructions to use it localy +1. To use this on local host download XAMPP (`https://www.apachefriends.org/download.html`) or MySQL database. +2. Set up .env file or modify `config.js` file to enter database credentials. +3. Open terminal in the project root and type `npm start` + + +## EndPoints Endpoint: `csclub.uwaterloo.ca/~phthakka/1pt-express` > Note: the old endpoint (`csclub.uwaterloo.ca/~phthakka/1pt`) is still live but will soon be **deprecated**. @@ -25,4 +32,8 @@ Endpoint: `csclub.uwaterloo.ca/~phthakka/1pt-express` } ``` -With this example 1pt.co/param will redirect to https://www.param.me +### `/r/` + +#### Method: `GET` + +###### With this endpoint 1pt.co/r/param will redirect to https://www.param.me \ No newline at end of file diff --git a/config.js b/config.js new file mode 100644 index 0000000..5e245ac --- /dev/null +++ b/config.js @@ -0,0 +1,6 @@ +export default { + HOST: process.env.PTHOST || "127.0.0.1", + USER: process.env.PTUSER ||'root', + PASSWORD: process.env.PTPASSWORD || '', + DB: process.env.PTDB || '1PTCO' +} \ No newline at end of file diff --git a/handleRequests.js b/handleRequests.js index 47daa09..555bce8 100644 --- a/handleRequests.js +++ b/handleRequests.js @@ -4,6 +4,7 @@ import generateRandomString from "./helpers/generateRandomString.js"; import isHarmful from "./helpers/safebrowsing.js" import urlExists from "./helpers/urlExists.js"; import verifyToken from "./helpers/verifyToken.js"; +import { addhttp, validURL } from "./helpers/urlUtils.js"; export const getURL = async (req, res) => { const data = (await query(`SELECT long_url FROM 1pt WHERE short_url = '${req.query.url}' LIMIT 1`))[0]; @@ -14,7 +15,7 @@ export const getURL = async (req, res) => { }) await query(`UPDATE 1pt SET hits=hits+1 WHERE short_url='${req.query.url}'`) - } else { + } else { res.status(404).send({ message: "URL doesn't exist!" }) @@ -53,13 +54,15 @@ export const addURL = async (req, res, logger) => { const ipAddress = req.ip; if (long === undefined || long === "") { - res.status(400).send({ - message: "Bad request", + return res.status(400).send({ + message: "parameter `long` is missing", }) - - return; } + if (!validURL(long)) return res.status(400).send({ + message: 'Malformed URL' + }) + let short; if (!requestedShort || await urlExists(requestedShort)) { @@ -78,11 +81,9 @@ export const addURL = async (req, res, logger) => { await query(`INSERT INTO 1pt (short_url, long_url, ip, email) VALUES ('${short}', '${long}', '${ipAddress}', '${email}')`); } catch { - res.status(401).send({ - message: "Unauthorized", + return res.status(401).send({ + message: "Unauthorized", }) - - return } } else { @@ -91,7 +92,7 @@ export const addURL = async (req, res, logger) => { logger.info(`Inserting ${short} -> ${long}`); - res.status(201).send({ + return res.status(201).send({ message: "Added!", short: short, long: long @@ -109,8 +110,17 @@ export const getProfileInfo = async (req, res) => { const user = await verifyToken(auth.split(" ")[1]); const email = user.email; - const data = await query(`SELECT short_url, long_url, timestamp, hits, ip, email FROM 1pt WHERE email = '${email}' ORDER BY timestamp DESC`); res.status(200).send(data) -} \ No newline at end of file +} + + +export const redirect = async (req, res) => { + const data = (await query(`SELECT long_url FROM 1pt WHERE short_url = '${req.params.shortCode}' LIMIT 1`))[0]; + if (data) { + return res.status(301).redirect(addhttp(data.long_url)) + } else { + return res.status(404).redirect('/') + } +} diff --git a/helpers/generateRandomString.js b/helpers/generateRandomString.js index dda5100..65f7960 100644 --- a/helpers/generateRandomString.js +++ b/helpers/generateRandomString.js @@ -10,7 +10,7 @@ const generateRandomString = async n => { const exists = await urlExists(randomString); - if (exists) { + if (exists) { randomString = await generateRandomString(n) } diff --git a/helpers/urlUtils.js b/helpers/urlUtils.js new file mode 100644 index 0000000..4ae8ac3 --- /dev/null +++ b/helpers/urlUtils.js @@ -0,0 +1,19 @@ +// source: https://stackoverflow.com/a/5717133 + +export const validURL = (str) => { + var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string + '(\\#[-a-z\\d_]*)?$','i'); // fragment locator + return pattern.test(str); + } + +// https://stackoverflow.com/a/24657561 +export const addhttp = (url) => { + if (!/^(?:f|ht)tps?\:\/\//.test(url)) { + url = "http://" + url; + } + return url; +} \ No newline at end of file diff --git a/helpers/verifyTable.js b/helpers/verifyTable.js new file mode 100644 index 0000000..dc4d77d --- /dev/null +++ b/helpers/verifyTable.js @@ -0,0 +1,8 @@ +import query from "./db.js"; +export const verifyTable = async(req,res,next) => { + const tableExistsQuery = `SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = '1ptco' AND table_name = '1pt';` + const insertTableQuery = `CREATE TABLE \`1pt\` (\`short_url\` varchar(10) NOT NULL,\`long_url\` varchar(256) NOT NULL,\`timestamp\` date NOT NULL DEFAULT current_timestamp(), \`hits\` int(11) NOT NULL, \`ip\` varchar(32) NOT NULL, \`email\` varchar(64) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;` + const exists = await query(tableExistsQuery) + if (!exists[0]) await query(insertTableQuery) + next() +} \ No newline at end of file diff --git a/index.js b/index.js index 98dd188..a168c40 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,12 @@ +import dotenv from "dotenv"; +dotenv.config() import express from "express"; import helmet from "helmet"; import cors from "cors"; import winston from "winston" -import { addURL, getInfo, getProfileInfo, getURL } from "./handleRequests.js"; +import { addURL, getInfo, getProfileInfo, getURL, redirect } from "./handleRequests.js"; import safeguard from "./helpers/safeguard.js"; +import { verifyTable } from "./helpers/verifyTable.js"; const PORT = 8000; @@ -25,7 +28,9 @@ app.get("/", (req, res) => { res.status(200).send("Welcome to the 1pt.co API! Read the docs at github.com/1pt-co/1pt"); }); -app.get("/getURL", (req, res) => safeguard(getURL, logger, req, res)); +app.use('*', verifyTable) + +app.get("/getURL" ,(req, res) => safeguard(getURL, logger, req, res)); app.get("/getInfo", (req, res) => safeguard(getInfo, logger, req, res)); @@ -33,6 +38,10 @@ app.post("/addURL", (req, res) => safeguard(addURL, logger, req, res)); app.get("/getProfileInfo", getProfileInfo); +// to replicate redirect functionality of frontend +app.get('/r/:shortCode', (req,res) => safeguard(redirect, logger, req, res)); + + app.listen( PORT, () => console.log(`1pt API running on http://localhost:${PORT}`) diff --git a/package-lock.json b/package-lock.json index 45dac40..88fa447 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "cors": "^2.8.5", + "dotenv": "^16.0.3", "express": "^4.18.2", "geoip-lite": "^1.4.6", "google-auth-library": "^8.7.0", @@ -439,6 +440,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1991,6 +2000,11 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" + }, "ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", diff --git a/package.json b/package.json index d5e4930..d782115 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "1pt.co API", "exports": "./index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node index.js" }, "repository": { "type": "git", @@ -19,6 +19,7 @@ "homepage": "https://github.com/1pt-co/api#readme", "dependencies": { "cors": "^2.8.5", + "dotenv": "^16.0.3", "express": "^4.18.2", "geoip-lite": "^1.4.6", "google-auth-library": "^8.7.0",