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