diff --git a/README.md b/README.md index 595dc02..8472994 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,66 @@ -# URL Shortener App -This is starter code for URL Shortener Project. +# URL Shortener -Fork this repository and use the given files to start with. +A URL shortener application that allows users to convert long URLs into shorter, more manageable links. -## Idea +## Features -A URL shortener website is a service that converts long website addresses into shorter, more manageable links. Users input a lengthy URL, and the website generates a condensed version, making it easier to share and remember. +- **URL Shortening**: Convert long URLs into short, easy-to-share links. +- **Custom Aliases**: Optionally create custom aliases for shortened URLs. +- **Redirects**: Redirect short URLs to their original long URLs. +- **URL History**: Display a list of previously shortened URLs. +- **Delete URLs**: Delete existing shortened URLs. +- **Alias Validation**: Prevent the use of an alias if it already exists. +- **Alias Restrictions**: Restrict special characters in aliases, allowing only alphanumeric characters and underscores (`_`). -## Interface +## Installation -The application interface consists of one page which contains: +### Prerequisites -* A form to shorten the URL, which takes two inputs: - - the long version of the url - - the alias of the url (defaults to a random string) -* A table which contains the previously shortened URLs. +- Node.js +- MongoDB -## Short URLs +### Steps -The short URLs are written in this form: +1. Clone the repository: + ```bash + git clone https://github.com/eyad-hazem-elmorsy/url-shortener.git + ``` +2. Navigate to the project directory: + ```bash + cd url-shortener + ``` +3. Install the dependencies: + ```bash + npm install + ``` +4. Start the application: + ```bash + npm start + ``` -``` -http://localhost:3000/{alias} -``` +## Usage -## Application Logic +1. Open your browser and go to `http://localhost:3000`. +2. Enter the long URL you wish to shorten. +3. Provide a custom alias. +4. Click "Shorten" to generate the short URL. +5. Use the short URL to redirect to the original long URL. +6. Manage your URLs, including deleting them and checking for alias availability. -* When a client tries to access the short URL, they should be redirected to the original long URL. -* If the client accesses a URL which doesn't exist, a `404` error should be displayed. -* There's no required authentication or authorization to generate short URLs. +## Project Structure -## Project Criteria +- `bin/`: Application entry point. +- `models/`: URL schema definition. +- `routes/`: Route definitions. +- `views/`: EJS templates for rendering. +- `app.js`: Main application file. +- `package.json`: Project metadata and dependencies. -- [ ] The application runs locally without any crashes -- [ ] The application logic is implemented correctly -- [ ] The application uses server-side rendering -- [ ] The application uses a MongoDB database +## Contributing -## Project Evaluation (50 pts.) +Contributions are welcome! Please fork the repository and submit a pull request. -* Project Completeness (25 pts.) -* Clean Code and Modulation (15 pts.) -* Descriptive Git Commit Messages (10 pts.) -* Nice touches (5 pts. bonus) \ No newline at end of file +--- + +Developed by [Eyad Hazem](https://github.com/eyad-hazem-elmorsy/). diff --git a/app.js b/app.js index 04be2f9..2b77ec1 100644 --- a/app.js +++ b/app.js @@ -3,8 +3,10 @@ const express = require('express'); const path = require('path'); const cookieParser = require('cookie-parser'); const logger = require('morgan'); +const mongoose = require('mongoose'); const indexRouter = require('./routes/index'); +const dbUrl = 'mongodb://localhost:27017/url-shortener' const app = express(); @@ -36,4 +38,13 @@ app.use(function (err, req, res, next) { res.render('error'); }); +// Connect to database +mongoose.connect(dbUrl) + .then(() => { + console.log('Connected to database'); + }) + .catch((err) => { + console.log(err.message); + }) + module.exports = app; diff --git a/models/shortener.js b/models/shortener.js new file mode 100644 index 0000000..ea8d409 --- /dev/null +++ b/models/shortener.js @@ -0,0 +1,17 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const shortenerSchema = new Schema({ + originalUrl: { + type: String, + required: true + }, + shortenedUrl: { + type: String, + required: true + } +}, { timestamps: true }); + +const Shortener = mongoose.model('Shortener', shortenerSchema); + +module.exports = Shortener; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6fd9de3..7f113f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,34 @@ "ejs": "^3.1.9", "express": "^4.18.2", "http-errors": "~1.6.3", + "mongoose": "^8.3.5", "morgan": "~1.9.1" }, "devDependencies": { "nodemon": "^3.0.1" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", + "integrity": "sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -195,6 +217,14 @@ "node": ">=8" } }, + "node_modules/bson": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz", + "integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -823,6 +853,14 @@ "node": ">=10" } }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -843,6 +881,11 @@ "node": ">= 0.6" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -897,6 +940,86 @@ "node": "*" } }, + "node_modules/mongodb": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.5.0.tgz", + "integrity": "sha512-Fozq68InT+JKABGLqctgtb8P56pRrJFkbhW0ux+x1mdHeyinor8oNzJqwLjV/t5X5nJGfTlluxfyMnOXNggIUA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.4.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.3.5.tgz", + "integrity": "sha512-2zqeAjHjCqT1o5HeUCvkE9tUHsXwemnwEZ2SKnUxsaP8p1a+UcSQSNbnSuOzUVePMwLETrsvLIRdFLjsNfCgWA==", + "dependencies": { + "bson": "^6.5.0", + "kareem": "2.6.3", + "mongodb": "6.5.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/morgan": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", @@ -912,6 +1035,46 @@ "node": ">= 0.8.0" } }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1062,6 +1225,14 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -1288,6 +1459,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1300,6 +1476,14 @@ "node": ">=10" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", @@ -1352,6 +1536,17 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1394,6 +1589,26 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 4c3762d..d31a156 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "start": "node ./bin/www" + "start": "nodemon ./bin/www" }, "dependencies": { "cookie-parser": "~1.4.4", @@ -11,6 +11,7 @@ "ejs": "^3.1.9", "express": "^4.18.2", "http-errors": "~1.6.3", + "mongoose": "^8.3.5", "morgan": "~1.9.1" }, "devDependencies": { diff --git a/routes/index.js b/routes/index.js index 5890929..b60b948 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,8 +1,59 @@ const router = require('express').Router(); +const Shortener = require('../models/shortener'); + +/* GET a page with alias. */ +router.get('/:alias', async function (req, res, next) { + const alias = req.params.alias; + const url = await Shortener.findOne({ shortenedUrl: alias }); + if (url) { + const originalUrl = url.originalUrl; + res.redirect(originalUrl); + } else { + res.status(404).send('404 Not Found'); + } +}); /* GET home page. */ -router.get('/', function (req, res, next) { +router.get('/', async function (req, res, next) { + const urls = await Shortener.find({}); + res.locals.urls = urls; + res.locals.domain = `${req.protocol}://${req.get('host')}/`; res.render('index', { title: 'Express' }); }); +// A middleware to validate alias +let validator = function (req, res, next) { + const regex = /^[a-zA-Z0-9_]+$/; + if (req.body.aliasInput.length > 3 && regex.test(req.body.aliasInput)) { + next(); + } else { + res.status(400).render('invalid'); + } +} + +// A middleware to check availability +let available = async function (req, res, next) { + if (await Shortener.findOne({ shortenedUrl: req.body.aliasInput })) { + res.status(400).render('not-available'); + } else { + next(); + } +} + +/* POST new url shortener from home page */ +router.post('/', validator, available, async (req, res, next) => { + const shortener = new Shortener({ + originalUrl: req.body.urlInput, + shortenedUrl: req.body.aliasInput + }); + await shortener.save(); + res.redirect('/'); +}); + +router.get('/:Id/delete', async (req, res, next) => { + console.log(req.params.Id); + await Shortener.findByIdAndDelete(req.params.Id); + res.redirect('/'); +}); + module.exports = router; diff --git a/views/index.ejs b/views/index.ejs index 70b7faa..7406581 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -74,19 +74,17 @@
diff --git a/views/invalid.ejs b/views/invalid.ejs new file mode 100644 index 0000000..6daff81 --- /dev/null +++ b/views/invalid.ejs @@ -0,0 +1,20 @@ + + + +
+ + +
+ + +
+ +