init, dotenv, mongo, working

This commit is contained in:
unurled 2022-02-23 12:29:46 +01:00
commit fe1417851d
17 changed files with 318376 additions and 0 deletions

3
.env.example Normal file
View file

@ -0,0 +1,3 @@
PORT="3000"
BASEURL="CHANGEME"
MONGO="MONGO_URI"

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.env
node_modules
package-lock.json
start.sh

11
README.md Normal file
View 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
View 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
View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}

33
public/index.html Normal file
View 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
View 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--*/

View 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

File diff suppressed because it is too large Load diff

27
public/nocompiled.css Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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: [],
}