├── lib ├── helpers.js └── myproxy.js ├── public ├── step3.png ├── index.css ├── featured.js ├── index.js └── index.html ├── deploy.config.js ├── package.json ├── .gitignore └── app.js /lib/helpers.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garageScript/FreeDomains/HEAD/public/step3.png -------------------------------------------------------------------------------- /lib/myproxy.js: -------------------------------------------------------------------------------- 1 | const adapter = {} 2 | adapter.getMappings = async () => { 3 | } 4 | 5 | module.exports = adapter 6 | -------------------------------------------------------------------------------- /deploy.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {"apps":{"name":"freedomains.dev","script":"npm run start:myproxy << /home/myproxy/.pm2/logs/freedomains.dev-out.log","instances":1,"autorestart":true,"watch":false,"max_memory_restart":"1G","env_production":{"NODE_ENV":"production","PORT":3002,"ADMIN":"A3uPmSvEs5H7FfgmjCUsqD3vsshwuRMwezneqXdA","WORKPATH":"/home/myproxy"},"error_file":"/home/myproxy/.pm2/logs/freedomains.dev-err.log","merge_logs":true}} 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freedomains.dev", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start:myproxy": "node app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "myproxy@freedomains.dev:/home/myproxy/freedomains.dev" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "express": "^4.21.2", 18 | "node-fetch": "^3.2.10", 19 | "uuid": "^3.3.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /public/index.css: -------------------------------------------------------------------------------- 1 | .deleteButton { 2 | float: right; 3 | display: inline-block; 4 | cursor: pointer; 5 | } 6 | .actionContainer .loading { 7 | display: none; 8 | } 9 | .actionContainer.isLoading .loading { 10 | display: inline-block; 11 | } 12 | .actionContainer.isLoading .setUpButton { 13 | display: none; 14 | } 15 | .save { 16 | margin-bottom: 10px; 17 | } 18 | 19 | .inlineCode { 20 | display: inline-block; 21 | padding: 5px; 22 | background: #eee; 23 | color: #f55; 24 | } 25 | 26 | .indent { 27 | padding-left: 20px; 28 | } 29 | .indent2 { 30 | padding-left: 40px; 31 | } 32 | 33 | .groupStep { 34 | margin-top: 10px; 35 | margin-bottom: 10px; 36 | } 37 | 38 | #newPortButton { 39 | height: 38px; 40 | } 41 | 42 | .step2 { 43 | color: #181; 44 | } 45 | 46 | h5 { 47 | margin-top: 10px; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /public/featured.js: -------------------------------------------------------------------------------- 1 | const appLink = document.querySelector('.appLink') 2 | 3 | const renderNext = (str, orig, cb, i = 0) => { 4 | if (i >= str.length && i >= orig.length) { 5 | return cb() 6 | } 7 | 8 | const oldChars = orig.split('') 9 | const newChar = i >= str.length ? ' ' : str[i] 10 | oldChars[i] = newChar 11 | 12 | const newDisplay = oldChars.join('') 13 | appLink.innerText = newDisplay 14 | 15 | setTimeout(() => { 16 | return renderNext(str, newDisplay, cb, i + 1) 17 | }, 100) 18 | } 19 | 20 | const doAll = (arr, i = 0) => { 21 | if (i >= arr.length) { 22 | i = 0 23 | } 24 | renderNext(arr[i].name, appLink.innerText, () => { 25 | appLink.href = arr[i].url 26 | setTimeout(() => { 27 | return doAll(arr, i + 1) 28 | }, 2000) 29 | }) 30 | } 31 | 32 | const data = [{ 33 | name: "Herman's Portfolio", 34 | url: 'https://herman.hireme.fun' 35 | }, { 36 | name: 'Corny Jokes', 37 | url: 'https://cornyjokes.learnjs.tips' 38 | }] 39 | 40 | doAll(data) 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 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 | # TypeScript v1 declaration files 45 | typings/ 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 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | data.db 107 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const fs = require('fs') 3 | const fetch = require('node-fetch') 4 | const { exec } = require('child_process') 5 | const uuidv4 = require('uuid/v4') 6 | const app = express() 7 | app.use(express.static('public')) 8 | app.use(express.json()) 9 | 10 | const myProxyApi = process.env.MYPROXY_API 11 | const myProxyKey = process.env.MYPROXY_KEY 12 | 13 | const dataPath = './data.db' 14 | let data = {} 15 | fs.readFile(dataPath, (err, fileData) => { 16 | if (err) return 17 | try { 18 | data = JSON.parse(fileData) 19 | } catch (e) { 20 | console.log('parse error', e) 21 | } 22 | }) 23 | 24 | const getMappings = () => { 25 | return data.mappings || {} 26 | } 27 | 28 | const getFullDomain = (subDomain, domain) => { 29 | const prefix = subDomain ? `${subDomain}.` : '' 30 | return `${prefix}${domain}` 31 | } 32 | 33 | 34 | app.get('/isAvailable', (req, res) => { 35 | const { subDomain, domain } = req.query 36 | const fullDomain = getFullDomain(subDomain, domain) 37 | const allMappings = getMappings() 38 | const result = { 39 | isAvailable: true 40 | } 41 | if (allMappings[fullDomain]) { 42 | result.isAvailable = false 43 | } 44 | return res.json(result) 45 | }) 46 | 47 | app.get('/downloadConfig', (req, res) => { 48 | fetch(`${myProxyApi}/mappings/download/?fullDomain=${req.query.fullDomain}`, { 49 | headers: { 50 | authorization: myProxyKey 51 | } 52 | }).then(r => { 53 | r.body.pipe(res) 54 | res.setHeader('content-disposition', `attachment; filename="deploy.config.js"`) 55 | }) 56 | }) 57 | 58 | app.get('/api/logs/:type/:domain', (req, res) => { 59 | const { type, domain } = req.params 60 | fetch(`${myProxyApi}/logs/${type}/${domain}`, { 61 | headers: { 62 | authorization: myProxyKey 63 | } 64 | }).then(r => { 65 | res.setHeader('content-type', 'text/plain') 66 | r.body.pipe(res) 67 | }) 68 | }) 69 | 70 | app.delete('/api/logs/:domain', (req, res) => { 71 | const { domain } = req.params 72 | fetch(`${myProxyApi}/logs/${domain}`, { 73 | method: 'DELETE', 74 | headers: { 75 | authorization: myProxyKey 76 | } 77 | }).then(r => { 78 | r.body.pipe(res) 79 | }) 80 | }) 81 | 82 | app.get('/api/domains', (req, res) => { 83 | fetch (`${myProxyApi}/availableDomains`, { 84 | headers: { 85 | authorization: myProxyKey 86 | } 87 | }) 88 | .then(r => r.json()) 89 | .then(domains => res.json(domains)) 90 | }) 91 | 92 | app.use('/api/mappings', (req, res, next) => { 93 | const userId = req.headers.authorization 94 | if (!userId || !(data.users || {})[userId]) { 95 | return res.status(401).json({ message: 'user id is invalid' }) 96 | } 97 | req.user = { 98 | id: userId 99 | } 100 | next() 101 | }) 102 | 103 | app.get('/api/mappings', async (req, res) => { 104 | const originalMappings = await fetch(`${myProxyApi}/mappings`, { 105 | headers: { 106 | authorization: myProxyKey 107 | } 108 | }).then(r => r.json()) 109 | const originalMap = originalMappings.reduce((acc, mapping) => { 110 | acc[mapping.fullDomain] = mapping 111 | return acc 112 | }, {}) 113 | 114 | const allMappings = getMappings() 115 | const userMappings = Object.values(allMappings).filter(m => { 116 | return m.userId === req.user.id 117 | }).map((mapping) => { 118 | const original = originalMap[mapping.fullDomain] || {} 119 | mapping.status = original.status || mapping.status 120 | mapping.id = original.id || mapping.id 121 | return mapping 122 | }).sort((a, b) => b.createdAt - a.createdAt) 123 | res.json(userMappings) 124 | }) 125 | 126 | app.delete('/api/mappings/:id', async (req, res) => { 127 | const allMappings = getMappings() 128 | const mapping = Object.values(allMappings).find(m => { 129 | return m.id === req.params.id 130 | }) 131 | 132 | if (!mapping || mapping.userId !== req.user.id) { 133 | return res.status(401).json({ message: 'user id is invalid' }) 134 | } 135 | 136 | await fetch(`${myProxyApi}/mappings/${req.params.id}`, { 137 | method: 'DELETE', 138 | headers: { 139 | 'Content-Type': 'application/json', 140 | authorization: myProxyKey 141 | } 142 | }).then(r => r.json()).catch(e => { 143 | console.log('error for deleting mapping', e) 144 | }) 145 | 146 | delete allMappings[mapping.fullDomain] 147 | saveData() 148 | 149 | res.json(mapping) 150 | }) 151 | 152 | app.post('/api/mappings', async (req, res) => { 153 | const { subDomain, domain } = req.body 154 | if (!subDomain) return res.status(400).json({ message: 'Subdomain field is required.' }) 155 | 156 | const newMapping = await fetch(`${myProxyApi}/mappings`, { 157 | method: 'POST', 158 | headers: { 159 | 'Content-Type': 'application/json', 160 | authorization: myProxyKey 161 | }, 162 | body: JSON.stringify({ 163 | domain, subDomain 164 | }) 165 | }).then(r => r.json()).catch(e => { 166 | console.log('error for creating mapping', e) 167 | }) 168 | 169 | // DEV env: 170 | /* 171 | const newMapping = { 172 | fullDomain: getFullDomain(subDomain, domain), 173 | gitLink: 'demo.com', 174 | id: Date.now() 175 | } 176 | */ 177 | 178 | const { gitLink, id, fullDomain } = newMapping 179 | const mappings = getMappings() 180 | mappings[fullDomain] = { 181 | id, 182 | domain, 183 | subDomain, 184 | fullDomain, 185 | gitLink, 186 | userId: req.user.id, 187 | createdAt: Date.now() 188 | } 189 | data.mappings = mappings 190 | saveData() 191 | 192 | res.json(req.body) 193 | }) 194 | 195 | app.get('/api/users/:userId', (req, res) => { 196 | const users = data.users || {} 197 | res.json({ 198 | key: users[req.params.userId] 199 | }) 200 | }) 201 | 202 | const saveData = () => { 203 | return new Promise((resolve, reject) => { 204 | fs.writeFile(dataPath, JSON.stringify(data, null, 2), (err) => { 205 | if (err) return reject(err) 206 | resolve() 207 | }) 208 | }) 209 | } 210 | 211 | app.post('/api/sshKeys', async (req, res) => { 212 | let { userId, key } = req.body 213 | if (!key) return res.json({ error: 'invalid input' }) 214 | 215 | const users = data.users || {} 216 | 217 | const foundUser = Object.entries(users).find(([uid, sshKey]) => { 218 | return (sshKey === key) 219 | }) 220 | 221 | // User already exist 222 | if (foundUser && foundUser.length === 2) { 223 | return res.json({ 224 | userId: foundUser[0], 225 | key: foundUser[1] 226 | }) 227 | } 228 | 229 | const tmpFilePath = `${__dirname}/keys_${uuidv4()}` 230 | fs.writeFile(tmpFilePath, key, () => { 231 | exec(`ssh-keygen -lf ${tmpFilePath}`, (err, result) => { 232 | exec(`rm ${tmpFilePath}`, async () => { 233 | if (err) { 234 | return res.status(400).send({ 235 | message: 'SSH KEY is invalid. Run "cat ~/.ssh/id_rsa.pub" and submit the output of the command' 236 | }) 237 | } 238 | 239 | // Replace UserId 240 | if (users[userId]) { 241 | // Delete oldSshKey? 242 | // Decided not to, a user could have multiple 243 | // sshKeys on one browser 244 | } 245 | 246 | // Create new key 247 | await fetch(`${myProxyApi}/sshKeys`, { 248 | method: 'POST', 249 | headers: { 250 | 'Content-Type': 'application/json', 251 | authorization: myProxyKey 252 | }, 253 | body: JSON.stringify({ 254 | key 255 | }) 256 | }).then(r => r.json()).catch(e => { 257 | console.log('error for creating mapping', e) 258 | }) 259 | 260 | userId = uuidv4() 261 | users[userId] = key 262 | data.users = users 263 | await saveData() 264 | return res.json({ userId, key }) 265 | }) 266 | }) 267 | }) 268 | }) 269 | 270 | app.listen(process.env.PORT || 8123) 271 | -------------------------------------------------------------------------------- /public/index.js: -------------------------------------------------------------------------------- 1 | /* global fetch localStorage $, confirm, alert */ 2 | 3 | const hostSelector = document.querySelector('#hostSelector') 4 | const dropDownDomains = document.querySelector('.dropdown-menu') 5 | const createButton = document.querySelector('.create') 6 | const domainList = document.querySelector('.domainList') 7 | const sshKeySaveButton = document.querySelector('#saveSshKey') 8 | const sshKeyInput = document.querySelector('#sshKeyInput') 9 | const subDomain = document.querySelector('.subDomain') 10 | const invalidElement = document.querySelector('.invalid-feedback') 11 | const domainForm = document.getElementById('domainForm') 12 | 13 | let selectedHost = '' 14 | 15 | class DomainOption { 16 | constructor (domain) { 17 | const dropdownElement = document.createElement('button') 18 | dropdownElement.classList.add('dropdown-item') 19 | dropdownElement.textContent = domain 20 | dropdownElement.onclick = () => { 21 | hostSelector.innerText = domain 22 | selectedHost = domain 23 | checkAvailability() 24 | } 25 | 26 | dropDownDomains.appendChild(dropdownElement) 27 | } 28 | } 29 | 30 | class MappingItem { 31 | constructor (data, idx) { 32 | const mappingElement = document.createElement('li') 33 | let iconClass 34 | let iconColor 35 | // The variables below are to hide log related icons when pm2 is not 36 | // being used to monitor the apps. These apps will not have status since 37 | // they are not managed by pm2. 38 | let settingClass 39 | let logClass 40 | if (data.status === 'running') { 41 | iconClass = 'fa fa-circle mr-1 mt-1' 42 | iconColor = 'rgba(50,255,50,0.5)' 43 | logClass = 'fa fa-file-text-o ml-1 mt-1' 44 | settingClass = 'ml-1 fa fa-cog' 45 | } else if (data.status === 'not started') { 46 | iconClass = '' 47 | iconColor = 'transparent' 48 | } else { 49 | iconClass = 'fa fa-circle mr-1 mt-1' 50 | iconColor = 'rgba(255, 50, 50, 0.5)' 51 | logClass = 'fa fa-file-text-o ml-1 mt-1' 52 | settingClass = 'ml-1 fa fa-cog' 53 | } 54 | mappingElement.classList.add( 55 | 'list-group-item', 56 | 'd-flex', 57 | 'align-items-center' 58 | ) 59 | domainList.appendChild(mappingElement) 60 | 61 | const createdDate = new Date(data.createdAt || Date.now()) 62 | let step2Content = '' 63 | if (idx) { 64 | step2Content = ` 65 | 66 | ${data.gitLink} 67 | 68 | ` 69 | } else { 70 | step2Content = ` 71 | Step 2 --> 72 | git clone 73 | 74 | ${data.gitLink} 75 | 76 | 77 | ` 78 | } 79 | mappingElement.innerHTML = ` 80 |
81 |
82 | 83 | 84 | 85 | ${data.fullDomain} 86 | 87 | 88 | ${createdDate.toDateString()} 89 | 90 | 95 | 96 | 101 | 102 |
103 | ${step2Content} 104 |
105 | 111 | ` 112 | 113 | const delButton = mappingElement.querySelector('.deleteButton') 114 | delButton.onclick = () => { 115 | if (confirm('Are you sure want to delete this domain?')) { 116 | apiFetch(`/api/mappings/${data.id}`, { 117 | method: 'DELETE', 118 | headers: { 119 | 'Content-Type': 'application/json' 120 | } 121 | }).then(res => { 122 | window.location.reload() 123 | }) 124 | } 125 | } 126 | } 127 | } 128 | 129 | const getDomainNames = () => { 130 | return fetch('/api/domains').then(r => r.json()) 131 | } 132 | 133 | const apiFetch = (url, options = {}) => { 134 | options.headers = options.headers || {} 135 | options.headers.authorization = userId 136 | return fetch(url, options).then(res => { 137 | if (res.status >= 400) { 138 | return res.json().then(response => { 139 | alert(response.message) 140 | localStorage.removeItem('freedomains') 141 | window.location.reload() 142 | }) 143 | } 144 | return res.json() 145 | }) 146 | } 147 | 148 | const getMappings = () => { 149 | return apiFetch('/api/mappings') 150 | } 151 | 152 | const startApp = () => { 153 | getDomainNames().then(list => { 154 | list.forEach(({ domain }) => { 155 | return new DomainOption(domain) 156 | }) 157 | selectedHost = list[0].domain 158 | hostSelector.innerText = list[0].domain 159 | }) 160 | getMappings().then(list => { 161 | list.forEach((dd, idx) => { 162 | return new MappingItem(dd, idx) 163 | }) 164 | }) 165 | 166 | domainForm.addEventListener('submit', (e) => { 167 | e.preventDefault() 168 | if (!subDomain.value) return alert('Subdomain field is required.') 169 | checkAvailability().then((d) => { 170 | if (!d.isAvailable) { 171 | return 172 | } 173 | apiFetch('/api/mappings', { 174 | method: 'POST', 175 | body: JSON.stringify({ 176 | domain: selectedHost, 177 | subDomain: subDomain.value 178 | }), 179 | headers: { 180 | 'Content-Type': 'application/json' 181 | } 182 | }).then(res => { 183 | window.location.reload() 184 | }) 185 | subDomain.value = '' 186 | }) 187 | }) 188 | } 189 | 190 | const checkAvailability = () => { 191 | const sDomain = subDomain.value 192 | return fetch(`/isAvailable?domain=${selectedHost}&subDomain=${sDomain}`).then(r => r.json()).then(d => { 193 | if (d.isAvailable) { 194 | invalidElement.innerText = ' ' 195 | subDomain.classList.remove('is-invalid') 196 | } else { 197 | const prefix = sDomain ? sDomain + '.' : '' 198 | invalidElement.innerText = `${prefix}${selectedHost} is not available. Please pick another!` 199 | subDomain.classList.add('is-invalid') 200 | } 201 | return d 202 | }) 203 | } 204 | subDomain.addEventListener('keyup', checkAvailability) 205 | subDomain.addEventListener('blur', checkAvailability) 206 | 207 | let userId = localStorage.getItem('freedomains') 208 | if (!userId) { 209 | $('#exampleModal').modal('show') 210 | } else { 211 | fetch(`/api/users/${userId}`).then(r => r.json()).then(data => { 212 | if (!data.key) { 213 | return $('#exampleModal').modal('show') 214 | } 215 | sshKeyInput.value = data.key 216 | startApp() 217 | }) 218 | } 219 | 220 | // SSH KEY Logic 221 | let saving = false 222 | sshKeySaveButton.addEventListener('click', () => { 223 | if (saving) { 224 | return 225 | } 226 | saving = true 227 | apiFetch(`/api/sshKeys`, { 228 | method: 'POST', 229 | headers: { 230 | 'Content-Type': 'application/json' 231 | }, 232 | body: JSON.stringify({ 233 | userId, 234 | key: sshKeyInput.value 235 | }) 236 | }).then(jResponse => { 237 | userId = jResponse.userId 238 | localStorage.setItem('freedomains', userId) 239 | saving = false 240 | $('#exampleModal').modal('hide') 241 | window.location.reload() 242 | }).catch(() => { 243 | saving = false 244 | $('#exampleModal').modal('hide') 245 | }) 246 | }) 247 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FreeDomains 6 | 12 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |

Free Domains For Your Apps

25 |

26 | This is a shared resource, please use responsibily. To ensure fair use and reduce costs, inactive Apps will be automatically removed after 7 days. 27 |

28 |

29 | This app is powered by 30 | MyProxy 31 |

32 | 33 |

34 | Featured Apps: 35 | Corny Jokes 36 |
37 | Want to be featured? Email Herman at 38 | hwong.0305@gmail.com 39 |

40 | 41 |

Step 1: Pick a domain name

42 |

This will be the domain name that your friends and family will be putting into the browser to visit your website.

43 |
44 | 45 |
46 | 50 |
51 |
52 |
53 | 54 |
55 | 62 | 63 |
64 | 74 | 75 |
76 | 77 |
Required
78 | 79 |
80 | 81 | 88 |
89 |
90 |
91 |
92 |
    93 |
    94 |
    95 |
    96 |
    97 |
    98 |

    Step 3: Deploy!

    99 |
    3a: Go into the downloaded folder
    100 |
    101 | git clone ... from step 2 would have downloaded a folder. Go into it. 102 |
    103 | If your url was new.n00b.city, then you should type: 104 | cd new.n00b.city 105 |
    106 | 107 |
    3b: Initialize your project
    108 |
    109 | Initialize your project with the following command. A new file (package.json) will be created for you. 110 |
    111 | npm init -y 112 |
    113 | 114 |
    3c: Create a simple javascript file
    115 |
    116 | Create a file. For example, 117 | app.js 118 |
    119 | Put the following code into it. This just sends back "hello" with every request 120 |
    
    121 | const express = require('express')
    122 | const app = express()
    123 | app.use(express.static('public'))
    124 | app.get('/', (req, res) => {
    125 |   res.send('hello');
    126 | });
    127 | 
    128 | app.listen(process.env.PORT || 8123);
    129 |           
    130 |
    131 | 132 |
    3d: Install dependencies
    133 |
    134 | The code above uses a library called express. You need to install it and save this dependency to package.json. 135 |
    136 | npm i --save express 137 |
    138 | 139 |
    3e: Tell MyProxy Server how to run your app
    140 |
    141 | Under the "scripts" section of package.json file, tell us how to run your app. 142 |
    
    143 | ...
    144 | "scripts": {
    145 |   "start": "node app.js",
    146 |   ...
    147 | }
    148 | ...
    149 |           
    150 |
    151 | 152 |
    3f: Save and push!
    153 |
    154 | Add all files to git: 155 | git add . 156 |
    157 |
    158 | Save your changes 159 | git commit -m "add my first app" 160 |
    161 |
    162 | Push! 163 | git push origin master 164 |
    165 | 166 |
    4. Celebrate
    167 |
    168 | Tell all your friends about your new website! 169 |
    170 | 171 |
    172 |
    173 |
    174 | 175 |
    176 |
    177 |

    Video Overview

    178 |
    179 |

    180 | The video below shows you how to deploy your apps after setting up your SSH Key. The contents may contain more information than the steps above because they are taken from the MyProxy site, but it should be enough to help you understand each steps. 181 |

    182 |
    183 | 184 |
    185 |
    186 |
    187 |
    188 | 189 |
    190 | 191 | 241 | 242 | 247 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 266 | 267 | 268 | 269 | --------------------------------------------------------------------------------