init, dotenv, mongo, working
This commit is contained in:
commit
fe1417851d
17 changed files with 318376 additions and 0 deletions
3
.env.example
Normal file
3
.env.example
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
PORT="3000"
|
||||||
|
BASEURL="CHANGEME"
|
||||||
|
MONGO="MONGO_URI"
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.env
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
start.sh
|
11
README.md
Normal file
11
README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# URL - Shortener
|
||||||
|
|
||||||
|
## How to Use ?
|
||||||
|
|
||||||
|
1. you need [mongdb](https://www.mongodb.com/en-us), and [nodejs](https://nodejs.org/en/)
|
||||||
|
|
||||||
|
2. `npm install`
|
||||||
|
|
||||||
|
3. `cp .env.example .env` change the port or the url in `.env`:
|
||||||
|
|
||||||
|
4. `npm start`
|
27
package.json
Normal file
27
package.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "short.it",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "src/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "postcss public/nocompiled.css -o public/main.css",
|
||||||
|
"start": "node src/main.js"
|
||||||
|
},
|
||||||
|
"author": "unurled",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.0.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"mongoose": "^6.0.5",
|
||||||
|
"prom-client": "^14.0.1",
|
||||||
|
"short-id": "^0.1.0-1",
|
||||||
|
"shortid": "^2.2.16",
|
||||||
|
"valid-url": "^1.0.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.0",
|
||||||
|
"postcss": "^8.4.4",
|
||||||
|
"postcss-cli": "^9.0.2",
|
||||||
|
"tailwindcss": "^2.2.19"
|
||||||
|
}
|
||||||
|
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
}
|
||||||
|
}
|
33
public/index.html
Normal file
33
public/index.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="public/main.css">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto">
|
||||||
|
<title>URL Shortener</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-coolgray-800 text-pink-200">
|
||||||
|
<div class="form">
|
||||||
|
<form action="" id="form">
|
||||||
|
<h2>URL Shortener</h2>
|
||||||
|
<div class="form-longurl">
|
||||||
|
<label for="longUrl">Long Url</label>
|
||||||
|
<input class="bg-coolgray-700 text-emerald-400 focus:border-coolgray-50 rounded-lg" type="text" id="longUrl" placeholder="www.example.com/very-long-url/and/totally/hard-to-remember" name="longUrl">
|
||||||
|
</div>
|
||||||
|
<div class="form-button">
|
||||||
|
<button type="submit" class="btn-submit hover:bg-red-700 focus:bg-red-700" id="btnSubmit">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="response">
|
||||||
|
<br/>
|
||||||
|
<p id="longUrl"></p>
|
||||||
|
<a id="shortUrl" class="text-emerald-500" href=""></a>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="public/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
56
public/js/app.js
Normal file
56
public/js/app.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import FetchService from './service/FetchService.js';
|
||||||
|
|
||||||
|
/*-- Objects --*/
|
||||||
|
const fetchService = new FetchService();
|
||||||
|
/*-- /Objects --*/
|
||||||
|
|
||||||
|
/*--Functions--*/
|
||||||
|
async function submitForm(e, form) {
|
||||||
|
// 1. Prevent reloading page
|
||||||
|
e.preventDefault();
|
||||||
|
// 2. Submit the form
|
||||||
|
// 2.1 User Interaction
|
||||||
|
const btnSubmit = document.getElementById('btnSubmit');
|
||||||
|
btnSubmit.disabled = true;
|
||||||
|
setTimeout(() => btnSubmit.disabled = false, 2000);
|
||||||
|
// 2.2 Build JSON body
|
||||||
|
const jsonFormData = buildJsonFormData(form);
|
||||||
|
// 2.3 Build Headers
|
||||||
|
const headers = buildHeaders();
|
||||||
|
// 2.4 Request & Response
|
||||||
|
const response = await fetchService.performPostHttpRequest(window.location.href + `api/url/shorten`, headers, jsonFormData); // Uses JSON Placeholder
|
||||||
|
// 2.5 Inform user of result
|
||||||
|
if(response) {
|
||||||
|
document.getElementById("shortUrl").innerHTML = response.shortUrl
|
||||||
|
document.getElementById("shortUrl").href = response.shortUrl
|
||||||
|
document.getElementById("longUrl").innerHTML = "Long Url: " + response.longUrl
|
||||||
|
}
|
||||||
|
else
|
||||||
|
alert(`An error occured.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHeaders(authorization = null) {
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": (authorization) ? authorization : "Bearer TOKEN_MISSING"
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildJsonFormData(form) {
|
||||||
|
const jsonFormData = { };
|
||||||
|
for(const pair of new FormData(form)) {
|
||||||
|
jsonFormData[pair[0]] = pair[1];
|
||||||
|
}
|
||||||
|
return jsonFormData;
|
||||||
|
}
|
||||||
|
/*--/Functions--*/
|
||||||
|
|
||||||
|
/*--Event Listeners--*/
|
||||||
|
const sampleForm = document.querySelector("#form");
|
||||||
|
if(sampleForm) {
|
||||||
|
sampleForm.addEventListener("submit", function(e) {
|
||||||
|
submitForm(e, this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/*--/Event Listeners--*/
|
62
public/js/service/FetchService.js
Normal file
62
public/js/service/FetchService.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
export default class FetchService {
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async performGetHttpRequest(fetchLink, headers, query=null) {
|
||||||
|
if(!fetchLink || !headers) {
|
||||||
|
throw new Error("One or more GET request parameters was not passed.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const rawResponse = await fetch(fetchLink, {
|
||||||
|
method: "GET",
|
||||||
|
headers: headers,
|
||||||
|
query: (query != null) ? query : ""
|
||||||
|
});
|
||||||
|
const content = await rawResponse.json();
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.error(`Error at fetch GET: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async performPostHttpRequest(fetchLink, headers, body) {
|
||||||
|
if(!fetchLink || !headers || !body) {
|
||||||
|
throw new Error("One or more POST request parameters was not passed.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const rawResponse = await fetch(fetchLink, {
|
||||||
|
method: "POST",
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
const content = await rawResponse.json();
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.error(`Error at fetch POST: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async performPutHttpRequest(fetchLink, headers, body) {
|
||||||
|
if(!fetchLink || !headers || !body) {
|
||||||
|
throw new Error("One or more POST request parameters was not passed.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const rawResponse = await fetch(fetchLink, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
const content = await rawResponse.json();
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.error(`Error at fetch PUT: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
317869
public/main.css
Normal file
317869
public/main.css
Normal file
File diff suppressed because it is too large
Load diff
27
public/nocompiled.css
Normal file
27
public/nocompiled.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 25px;
|
||||||
|
/*background-color: #00171F;
|
||||||
|
color: #AEB8FE;*/
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin: 8px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #2643d6;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 2rem;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 1rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
14
src/config/db.config.js
Normal file
14
src/config/db.config.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
require('dotenv').config()
|
||||||
|
// import mongoose package
|
||||||
|
const mongoose = require('mongoose')
|
||||||
|
|
||||||
|
// declare a Database string URI
|
||||||
|
const DB_URI = process.env.MONGO
|
||||||
|
|
||||||
|
// establishing a database connection
|
||||||
|
mongoose.connect(DB_URI)
|
||||||
|
|
||||||
|
const connection = mongoose.connection
|
||||||
|
|
||||||
|
// export the connection object
|
||||||
|
module.exports = connection
|
90
src/main.js
Normal file
90
src/main.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
require('dotenv').config()
|
||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const url = require('url')
|
||||||
|
const app = express();
|
||||||
|
const client = require('prom-client');
|
||||||
|
|
||||||
|
const collectDefaultMetrics = client.collectDefaultMetrics;
|
||||||
|
const register = new client.Registry();
|
||||||
|
client.collectDefaultMetrics({
|
||||||
|
app: 'url',
|
||||||
|
prefix: 'node_',
|
||||||
|
timeout: 10000,
|
||||||
|
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5],
|
||||||
|
register
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpRequestTimer = new client.Histogram({
|
||||||
|
name: 'http_request_duration_seconds',
|
||||||
|
help: 'Duration of HTTP requests in seconds',
|
||||||
|
labelNames: ['method', 'route', 'code'],
|
||||||
|
buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10] // 0.1 to 10 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDelayHandler = async (req, res) => {
|
||||||
|
if ((Math.floor(Math.random() * 100)) === 0) {
|
||||||
|
throw new Error('Internal Error')
|
||||||
|
}
|
||||||
|
|
||||||
|
// delay for 3-6 seconds
|
||||||
|
const delaySeconds = Math.floor(Math.random() * (6 - 3)) + 3
|
||||||
|
await new Promise(res => setTimeout(res, delaySeconds * 1000))
|
||||||
|
|
||||||
|
res.end('Slow url accessed !!');
|
||||||
|
};
|
||||||
|
|
||||||
|
register.registerMetric(httpRequestTimer);
|
||||||
|
|
||||||
|
// Database config
|
||||||
|
const connection = require('./config/db.config');
|
||||||
|
connection.once('open', () => console.log('DB Connected'));
|
||||||
|
connection.on('error', () => console.log('Error'));
|
||||||
|
|
||||||
|
// Routes Config
|
||||||
|
app.use(express.json({
|
||||||
|
extended: false
|
||||||
|
})); //parse incoming request body in JSON format.
|
||||||
|
|
||||||
|
app.get('/metrics', async (req, res) => {
|
||||||
|
// Start the timer
|
||||||
|
const end = httpRequestTimer.startTimer();
|
||||||
|
const route = req.route.path;
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', register.contentType);
|
||||||
|
res.send(await register.metrics());
|
||||||
|
|
||||||
|
// End timer and add labels
|
||||||
|
end({ route, code: res.statusCode, method: req.method });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/slow', async (req, res) => {
|
||||||
|
// Start the timer
|
||||||
|
const end = httpRequestTimer.startTimer();
|
||||||
|
const route = req.route.path;
|
||||||
|
|
||||||
|
await createDelayHandler(req, res);
|
||||||
|
|
||||||
|
// End timer and add labels
|
||||||
|
end({ route, code: res.statusCode, method: req.method });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/', require('./service/redirect'));
|
||||||
|
app.use('/api/url', require('./service/url'));
|
||||||
|
app.get('/', function(req, res) {
|
||||||
|
res.sendFile(path.join(__dirname + `../../public/index.html`))
|
||||||
|
});
|
||||||
|
app.get('/public/js/app.js', function(req, res) {
|
||||||
|
res.sendFile(path.join(__dirname + `../../public/js/app.js`))
|
||||||
|
});
|
||||||
|
app.get('/public/js/service/FetchService.js', function(req, res) {
|
||||||
|
res.sendFile(path.join(__dirname + `../../public/js/service/FetchService.js`));
|
||||||
|
});
|
||||||
|
app.get('/public/main.css', function(req, res) {
|
||||||
|
res.sendFile(path.join(__dirname + `../../public/main.css`));
|
||||||
|
});
|
||||||
|
|
||||||
|
//Listen for incoming requests
|
||||||
|
const PORT = process.env.PORT;
|
||||||
|
app.listen(PORT, console.log(`server started, listening PORT ${PORT}`));
|
||||||
|
console.log(`Access Server at: https://localhost:${PORT}`);
|
15
src/models/url.js
Normal file
15
src/models/url.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
const mongoose = require('mongoose')
|
||||||
|
|
||||||
|
// instantiate a mongoose schema
|
||||||
|
const URLSchema = new mongoose.Schema({
|
||||||
|
urlCode: String,
|
||||||
|
longUrl: String,
|
||||||
|
shortUrl: String,
|
||||||
|
date: {
|
||||||
|
type: String,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// create a model from schema and export it
|
||||||
|
module.exports = mongoose.model('Url', URLSchema)
|
36
src/service/redirect.js
Normal file
36
src/service/redirect.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const express = require('express')
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
const Url = require('../models/url')
|
||||||
|
|
||||||
|
// : app.get(/:code)
|
||||||
|
|
||||||
|
// @route GET /:code
|
||||||
|
// @description Redirect to the long/original URL
|
||||||
|
router.get('/:code', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// find a document match to the code in req.params.code
|
||||||
|
const url = await Url.findOne({
|
||||||
|
urlCode: req.params.code
|
||||||
|
})
|
||||||
|
if (url) {
|
||||||
|
// when valid we perform a redirect
|
||||||
|
return res.redirect(url.longUrl)
|
||||||
|
} else {
|
||||||
|
return res.sendFile(path.join(__dirname + `../../../public/index.html`))
|
||||||
|
// else return a not found 404 status
|
||||||
|
//return res.status(404).json('No URL Found')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// exception handler
|
||||||
|
catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
res.status(500).json('Server Error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router
|
8
src/service/server.js
Normal file
8
src/service/server.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
require('dotenv').config()
|
||||||
|
const express = require('express')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
const PORT = process.env.PORT
|
||||||
|
// Listen for incoming requests
|
||||||
|
app.listen(PORT, () => console.log(`server started, listening PORT ${PORT}`))
|
71
src/service/url.js
Normal file
71
src/service/url.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
require('dotenv').config()
|
||||||
|
// packages needed in this file
|
||||||
|
const express = require('express')
|
||||||
|
const validUrl = require('valid-url')
|
||||||
|
const shortid = require('shortid')
|
||||||
|
|
||||||
|
// creating express route handler
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// import the Url database model
|
||||||
|
const Url = require('../models/url')
|
||||||
|
|
||||||
|
// @route POST /api/url/shorten
|
||||||
|
// @description Create short URL
|
||||||
|
|
||||||
|
// The API base Url endpoint
|
||||||
|
const baseUrl = process.env.BASEURL
|
||||||
|
|
||||||
|
router.post('/shorten', async (req, res) => {
|
||||||
|
const {
|
||||||
|
longUrl
|
||||||
|
} = req.body // destructure the longUrl from req.body.longUrl
|
||||||
|
|
||||||
|
// check base url if valid using the validUrl.isUri method
|
||||||
|
if (!validUrl.isUri(baseUrl)) {
|
||||||
|
return res.status(401).json('Invalid base URL')
|
||||||
|
}
|
||||||
|
|
||||||
|
// if valid, we create the url code
|
||||||
|
const urlCode = shortid.generate()
|
||||||
|
|
||||||
|
// check long url if valid using the validUrl.isUri method
|
||||||
|
if (validUrl.isUri(longUrl)) {
|
||||||
|
try {
|
||||||
|
/* The findOne() provides a match to only the subset of the documents
|
||||||
|
in the collection that match the query. In this case, before creating the short URL,
|
||||||
|
we check if the long URL was in the DB ,else we create it.
|
||||||
|
*/
|
||||||
|
let url = await Url.findOne({
|
||||||
|
longUrl
|
||||||
|
})
|
||||||
|
|
||||||
|
// url exist and return the response
|
||||||
|
if (url) {
|
||||||
|
res.json(url)
|
||||||
|
} else {
|
||||||
|
// join the generated short code the the base url
|
||||||
|
const shortUrl = baseUrl + '/' + urlCode
|
||||||
|
|
||||||
|
// invoking the Url model and saving to the DB
|
||||||
|
url = new Url({
|
||||||
|
longUrl,
|
||||||
|
shortUrl,
|
||||||
|
urlCode,
|
||||||
|
date: new Date()
|
||||||
|
})
|
||||||
|
await url.save()
|
||||||
|
res.json(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// exception handler
|
||||||
|
catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
res.status(500).json('Server Error')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(401).json('Invalid longUrl')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
44
tailwind.config.js
Normal file
44
tailwind.config.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
const { coolGray } = require('tailwindcss/colors')
|
||||||
|
const colors = require('tailwindcss/colors')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
purge: [],
|
||||||
|
darkMode: false, // or 'media' or 'class'
|
||||||
|
theme: {
|
||||||
|
colors: {
|
||||||
|
inherit: colors.inherit,
|
||||||
|
current: colors.current,
|
||||||
|
transparent: colors.transparent,
|
||||||
|
black: colors.black,
|
||||||
|
white: colors.white,
|
||||||
|
slate: colors.slate,
|
||||||
|
gray: colors.gray,
|
||||||
|
zinc: colors.zinc,
|
||||||
|
neutral: colors.neutral,
|
||||||
|
stone: colors.stone,
|
||||||
|
red: colors.red,
|
||||||
|
orange: colors.orange,
|
||||||
|
amber: colors.amber,
|
||||||
|
yellow: colors.yellow,
|
||||||
|
lime: colors.lime,
|
||||||
|
green: colors.green,
|
||||||
|
emerald: colors.emerald,
|
||||||
|
teal: colors.teal,
|
||||||
|
cyan: colors.cyan,
|
||||||
|
sky: colors.sky,
|
||||||
|
blue: colors.blue,
|
||||||
|
indigo: colors.indigo,
|
||||||
|
violet: colors.violet,
|
||||||
|
purple: colors.purple,
|
||||||
|
fuchsia: colors.fuchsia,
|
||||||
|
pink: colors.pink,
|
||||||
|
rose: colors.rose,
|
||||||
|
bluegray: colors.blueGray,
|
||||||
|
coolgray: colors.coolGray
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
Loading…
Reference in a new issue