├── package.json ├── public ├── 404.css ├── 404.html ├── app.js ├── index.html └── styles.css ├── .gitignore └── index.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "dev": "nodemon index.js" 9 | }, 10 | "keywords": [], 11 | "author": "CJ R. (https://w3cj.sh)", 12 | "license": "MIT", 13 | "dependencies": { 14 | "dotenv": "^8.2.0", 15 | "express": "^4.17.1", 16 | "express-rate-limit": "^5.1.3", 17 | "express-slow-down": "^1.3.1", 18 | "helmet": "^3.22.0", 19 | "monk": "^7.3.0", 20 | "morgan": "^1.10.0", 21 | "nanoid": "^3.1.9", 22 | "nodemon": "^2.0.4", 23 | "yup": "^0.29.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/404.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | height: 100%; 10 | } 11 | 12 | body { 13 | font-family: 'Poppins', sans-serif; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | background-color: #262626; 19 | } 20 | 21 | .not-found { 22 | margin: 1rem 0; 23 | color: #ffffff; 24 | font-size: 3rem; 25 | font-weight: normal; 26 | } 27 | 28 | .go-home-link { 29 | display: flex; 30 | align-items: center; 31 | color: #ffffff; 32 | font-size: 1.5rem; 33 | } 34 | 35 | .arrow { 36 | margin-right: 1rem; 37 | transition: transform 0.2s ease-in-out; 38 | } 39 | 40 | .go-home-link:hover .arrow { 41 | transform: translateX(10px); 42 | } 43 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cdg.sh 7 | 8 | 9 | 10 | 11 | 12 |

not found

13 | 14 | 15 | 16 | 17 | go home 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | const app = new Vue({ 2 | el: '#app', 3 | data: { 4 | url: '', 5 | slug: '', 6 | error: '', 7 | formVisible: true, 8 | created: null, 9 | }, 10 | methods: { 11 | async createUrl() { 12 | this.error = ''; 13 | const response = await fetch('/url', { 14 | method: 'POST', 15 | headers: { 16 | 'content-type': 'application/json', 17 | }, 18 | body: JSON.stringify({ 19 | url: this.url, 20 | slug: this.slug || undefined, 21 | }), 22 | }); 23 | if (response.ok) { 24 | const result = await response.json(); 25 | this.formVisible = false; 26 | this.created = `https://cdg.sh/${result.slug}`; 27 | } else if (response.status === 429) { 28 | this.error = 'You are sending too many requests. Try again in 30 seconds.'; 29 | } else { 30 | const result = await response.json(); 31 | this.error = result.message; 32 | } 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cdg.sh 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 |
15 |
16 | {{error}} 17 |
18 | 19 | 20 | 21 |
22 |
23 |

Your short url is: {{created}}

24 |
25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | height: 100%; 10 | } 11 | 12 | body { 13 | font-family: 'Poppins', sans-serif; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | background-color: #262626; 19 | } 20 | 21 | header { 22 | display: flex; 23 | justify-content: center; 24 | margin-bottom: 3rem; 25 | } 26 | 27 | .logo { 28 | max-width: 90%; 29 | image-rendering: -moz-crisp-edges; 30 | } 31 | 32 | .error { 33 | background: #A63446; 34 | padding: 0.5rem 1rem; 35 | color: #ffffff; 36 | margin-bottom: 1rem; 37 | text-align: center; 38 | } 39 | 40 | .form { 41 | display: flex; 42 | flex-direction: column; 43 | } 44 | 45 | .input, 46 | .create { 47 | margin: 1rem 0; 48 | max-width: 100%; 49 | } 50 | 51 | .input { 52 | font-family: inherit; 53 | padding-bottom: 1rem; 54 | background: none; 55 | border: none; 56 | color: #ffffff; 57 | border-bottom: 2px solid #ffffff; 58 | text-align: center; 59 | font-size: 1.25rem; 60 | transition: border-bottom-color 0.3s ease-in-out; 61 | caret-color: #56BC58; 62 | } 63 | 64 | .input:focus { 65 | outline: none; 66 | border-bottom-color: #56BC58; 67 | } 68 | 69 | .input::placeholder { 70 | color: #ffffff; 71 | opacity: 0.7; 72 | } 73 | 74 | .create { 75 | cursor: pointer; 76 | font-family: inherit; 77 | font-size: 1.15rem; 78 | color: #262626; 79 | border: none; 80 | background-color: #56BC58; 81 | padding: 0.75em 1.25rem; 82 | box-shadow: 3px 3px 0 0 #ffffff; 83 | transition: box-shadow 0.2s ease-in-out; 84 | } 85 | 86 | .create:hover { 87 | box-shadow: 0 0 0 0 #ffffff; 88 | } 89 | 90 | .created { 91 | color: #ffffff; 92 | } 93 | 94 | .created a { 95 | color: inherit; 96 | } 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel# Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | .vercel 118 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const morgan = require('morgan'); 4 | const helmet = require('helmet'); 5 | const yup = require('yup'); 6 | const monk = require('monk'); 7 | const rateLimit = require('express-rate-limit'); 8 | const slowDown = require('express-slow-down'); 9 | const { nanoid } = require('nanoid'); 10 | 11 | require('dotenv').config(); 12 | 13 | const db = monk(process.env.MONGODB_URI); 14 | const urls = db.get('urls'); 15 | urls.createIndex({ slug: 1 }, { unique: true }); 16 | 17 | const app = express(); 18 | app.enable('trust proxy'); 19 | 20 | app.use(helmet()); 21 | app.use(morgan('common')); 22 | app.use(express.json()); 23 | app.use(express.static('./public')); 24 | 25 | const notFoundPath = path.join(__dirname, 'public/404.html'); 26 | 27 | app.get('/v/:searchTerm', (req, res, next) => { 28 | res.redirect(`https://coding.garden/videos/#/?filter=` + req.params.searchTerm); 29 | }); 30 | 31 | app.get('/:id', async (req, res, next) => { 32 | const { id: slug } = req.params; 33 | try { 34 | const url = await urls.findOne({ slug: slug.toLowerCase() }); 35 | if (url) { 36 | return res.redirect(url.url); 37 | } 38 | return res.status(404).sendFile(notFoundPath); 39 | } catch (error) { 40 | return res.status(404).sendFile(notFoundPath); 41 | } 42 | }); 43 | 44 | const schema = yup.object().shape({ 45 | slug: yup.string().trim().matches(/^[\w\-]+$/i), 46 | url: yup.string().trim().url().required(), 47 | }); 48 | 49 | app.post('/url', slowDown({ 50 | windowMs: 30 * 1000, 51 | delayAfter: 1, 52 | delayMs: 500, 53 | }), rateLimit({ 54 | windowMs: 30 * 1000, 55 | max: 1, 56 | }), async (req, res, next) => { 57 | let { slug, url } = req.body; 58 | try { 59 | throw new Error('Url shortening is no longer open to the public.'); 60 | await schema.validate({ 61 | slug, 62 | url, 63 | }); 64 | if (url.includes('cdg.sh')) { 65 | throw new Error('Stop it. 🛑'); 66 | } 67 | if (!slug) { 68 | slug = nanoid(5); 69 | } else { 70 | const existing = await urls.findOne({ slug }); 71 | if (existing) { 72 | throw new Error('Slug in use. 🍔'); 73 | } 74 | } 75 | slug = slug.toLowerCase(); 76 | const newUrl = { 77 | url, 78 | slug, 79 | }; 80 | const created = await urls.insert(newUrl); 81 | res.json(created); 82 | } catch (error) { 83 | next(error); 84 | } 85 | }); 86 | 87 | app.use((req, res, next) => { 88 | res.status(404).sendFile(notFoundPath); 89 | }); 90 | 91 | app.use((error, req, res, next) => { 92 | if (error.status) { 93 | res.status(error.status); 94 | } else { 95 | res.status(500); 96 | } 97 | res.json({ 98 | message: error.message, 99 | stack: process.env.NODE_ENV === 'production' ? '🥞' : error.stack, 100 | }); 101 | }); 102 | 103 | const port = process.env.PORT || 1337; 104 | app.listen(port, () => { 105 | console.log(`Listening at http://localhost:${port}`); 106 | }); 107 | --------------------------------------------------------------------------------