brew search node
brew install node@6
# multiple versions
brew unlink node
brew link node@6
// Without Express - just Node.js
const { createServer } = require('http')
const PORT = process.env.PORT || 3003
const requestHandler = (req, res) => { res.end('Hello world') }
createServer(requestHandler).listen(PORT, () => console.log(`Node.js server running on http://localhost:${PORT}/`))
// With Express
const express = require('express')
const server = express()
server.get('*', requestHandler)
server.listen(PORT, () => console.log(`Express server running on http://localhost:${PORT}/`))
// Returns array like '/:0/:1/:2/:etc'
const parseRequestParams = url => (url.split('?')[0] || '/').substr(1).split('/')
// Returns a req.query-type object
const parseRequestQuery = url => (url.split('?')[1] || '')
.split('&')
.reduce((result, propValue) => {
const key = propValue.split('=')[0]
if (key) result[key] = propValue.split('=')[1]
return result
}, {})
router.get('/', function (req, res) {})
router.post('/', function (req, res) {})
router.put('/user', function (req, res) {})
router.delete('/user', function (req, res) {})
req.method // 'GET'
req.url = '/mypage?query=value'
req.query (url?key=value)
req.headers
host = 'www.myserver.com:8080'
'x-forwarded-host': 'www.myserver.com:8080',
'x-forwarded-proto': 'https',
'x-forwarded-port': '8080',
origin = 'http://localhost:3301'
referer = 'http://localhost:3301/my-page'
'user-agent'
req.body (JSON body)
// Get *relative* URL
const url = new URL(request.url);
const relativePath = url.pathname;
const { host, origin, referer } = req.headers
const getServerHref = (req) => `${req.headers.host.includes('localhost') ? 'http' : 'https'}://${req.headers.host}/`
Express:
req.originalUrl (Express)
req.path (Express)
req.params (url/:key)
Pure Node / Vercel:
aborted
body
client
complete
cookies
headers
httpVersion
httpVersionMajor
httpVersionMinor
method
query
rawHeaders
rawTrailers
socket
statusCode
statusMessage
trailers
upgrade
url
_consuming
_dumped
_events
_eventsCount
_maxListeners
_readableState
if (req.method !== 'POST') throw new CustomError('Method not allowed', 405) if (!ALLOW_ORIGINS.includes(req.headers.origin)) throw new CustomError('Request not authorized', 401)
https://stackoverflow.com/questions/10183291/how-to-get-the-full-url-in-express
var fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`
const typicalRoute = function (req, res, next) {
cacheProvider.setKeyOnResponse(res, get(req, 'crudify.user.account.reference'))
next()
next(new Error(...))
}
/** export default (req, res) => handleRestRequest(async (req, res) => {...}, { req, res }) */
module.exports.handleRestRequest = async function handleRestRequest (actionFunction, { req, res }) {
try {
await actionFunction(req, res)
} catch (error) {
const reference = `E${Math.round(1000 * Math.random())}`
const { message, status = 400 } = error
console.error(`[${reference}] Error ${status}: “${message}” –`, error)
if (!isNaN(status)) res.status(status)
res.json({ status, message, reference })
}
}
// From: https://levelup.gitconnected.com/the-definite-guide-to-handling-errors-gracefully-in-javascript-58424d9c60e6
/** throw new CustomError(`Account not found`, 404) */
module.exports.CustomError = class CustomError extends Error {
constructor (message, status = 400) {
super(message)
if (Error.captureStackTrace) Error.captureStackTrace(this, CustomError)
this.status = status
}
}
res.writeHead(200, {
'Content-Type': 'image/png',
'Content-Length': img.length
})
res.end(img)
res.writeHead(302, { Location: 'your/404/path.html' })
res.end()
res.statusCode = 404
res.statusMessage = 'Not found'
res.end(statusMessage)
res.writeHead(statusCode[, statusMessage][, headers])
// text/html, text/javascript, text/csv, application/json, image/svg+xml, image/jpeg
res.setHeader('content-type', 'text/javascript')
res.setHeader('cache-control', 'public, max-age=31557600') // One year
res.write()
res.end()
-
Set headers:
const setCORSHeaders = (res, methods = 'POST,OPTIONS') => { res.setHeader('Access-Control-Allow-Methods', methods) res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Access-Control-Allow-Headers', 'Content-Type') // 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' res.setHeader('Access-Control-Allow-Credentials', 'true') }
-
Respond to
OPTIONS
:
if (req.method === 'OPTIONS') { res.statusCode = 200 return res.json({}) }
res.status(302).end()
res.set('location', newUrl)
res.send = Express write() + end()
res.json(myObj)
console.log('Request:', _.pick(req, ['params', 'query', 'body']))
res.statusCode
NEW: res.status(404).send('Page not found')
NEW: res.status(404).json(myObj)
OLD: res.sendStatus(404, 'Page not found')
const https = require('https')
https.get(url, res => {
let data = ''
// A chunk of data has been received.
res.on('data', chunk => {
data += chunk
})
// The whole response has been received. Print out the result.
res.on('end', () => {
console.log(JSON.parse(data).explanation)
})
}).on('error', err => {
console.log('Error: ' + err.message)
})
request.get(url, { json: true }, function (err, response, body) {
response.statusCode
})
request.post(url, { form: body }, function (err, result, body) {
})
request({ method: 'PUT', url: url, json: obj }, function (err, res, body) {
})
Note: latest version requires import
, use yarn add node-fetch@2
if you need require
.
const fetch = require('node-fetch')
const userResponse = await fetch(`${API_URL}/api/users/${user}`)
const userJson = await userResponse.json() // or text(), arrayBuffer(), blob(), formData()
NEWER:
yarn add html-metadata-parser
import { parser as htmlMetadataParser } from 'html-metadata-parser'
Old:
const htmlMetadata = require('html-metadata')
const { general, openGraph, twitter, schemaOrg } = await htmlMetadata(url)
const { title, description, image } = (openGraph || general) // openGraph not always present
const cheerio = require('cheerio')
const $ = cheerio.load(htmlContent)
$(this).toString()
$(this).text()
$(this).find('.display-name').text()
// $(element) === $(this)
$('div').map(function (i, element) { console.log($(element).toString()) })
$element.children()
$element[0].name
$element.attr('class')
$.root().html()
https://www.npmjs.org/package/ejs
<%= variable %> // variable is evaluated and printed out (escaped)
<%- variable %> // variable is evaluated and printed out (HTML, not escaped)
<% variable %> // variable is evaluated but not printed out
<% if (true) { %>
<h1>foo</h1>
<% } else { %>
<h1>bar</h1>
<% } %>
<table>
<% for (var i=0; i < data.length; i++) { %>
<tr>
<td><%= data[i].id %></td>
<td><%= data[i].name %></td>
</tr>
<% } %>
</table>
// To string. Months are zero-based
app.locals.formatDate = function (dateObj) {
return (dateObj.getFullYear() + "-" + ('0' + (dateObj.getMonth()+1)).slice(-2) + "-" + ('0' + dateObj.getDate()).slice(-2) )
}
// Encode:
var b = new Buffer('JavaScript')
var s = b.toString('base64'); // "SmF2YVNjcmlwdA=="
// Decode:
var b = new Buffer('SmF2YVNjcmlwdA==', 'base64')
var s = b.toString(); // "JavaScript"
yarn add dotenv
import dotenv from 'dotenv'
dotenv.config({ path: '.env.local' })
require('dotenv').config() // load .env file
require('dotenv').config({ path: '.env.local' })
- ➡️ Client Sends Login Request: Client sends login credentials (e.g., username and password) to the server.
- ⬅️ Server Authenticates User and returns JWT: Server verifies the credentials. If valid, it generates a JWT. The server responds with a JWT, typically in a JSON response or in an HTTP-only cookie.
- Client Stores JWT: Client stores the token (e.g., in local storage, session storage, or a cookie).
- ➡️ Client Sends JWT with API Requests: For subsequent requests, the client includes the JWT in the Authorization header (
Authorization: Bearer [token]
). - ⬅️ Server Verifies JWT: Server checks the token’s signature and validates its claims (e.g., expiration time).
- Access Granted or Denied: If the token is valid, the server processes the request and sends a response. If invalid or expired, the server denies access or requests re-authentication.
- Client Handles Token Expiration: If expired, the client may need to refresh the token (e.g., using a refresh token) or prompt the user to log in again.
Client (header):
const jwt = await getUserJWT()
Object.assign(defaultHeaders, { Authorization: `Bearer ${jwt}` })
// JWT Token with fetch()
window.fetch(`${config.appUrl}api/domains`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify(data)
})
Server - generate token:
yarn add jsonwebtoken
import jwt from 'jsonwebtoken'
// Sign/generate JWT
jwt.sign(payloadObjectOrString, secretOrPrivateKey, [options])
// Verify/decode
const decoded = jwt.verify(token, secretOrPublicKey, [options])
// Firebase JWT token
(new FirebaseTokenGenerator(process.env.MY_SECRET)).createToken(payloadObjectOrString)
Server - verify access:
const jwt = require('express-jwt') // https://github.com/auth0/express-jwt
module.exports.jwtAuthentication = jwt({
secret: process.env.MY_SECRET,
credentialsRequired: false // false = will let users through if auth fails
})
Scopes for access levels, e.g. Admin roles.
Note: doesn’t help import/export
yarn add @babel/core @babel/node @babel/preset-env
babel-node app.js
http://javascriptplayground.com/blog/2012/08/writing-a-command-line-node-tool/
process.env.NODE_ENV
#!/usr/bin/env node
'use strict'
console.log('process.argv', process.argv.length)
// Get all env values
Object.keys(process.env)
.map((key, i) => [key, Object.values(process.env)[i]])
.filter(([key]) => !key.startsWith('rvm_') && !key.startsWith('rvm_') && !key.startsWith('_system'))
.sort((a, b) => a[0] > b[0] ? 1 : (a[0] < b[0] ? -1 : 0))
.map(([key, value]) => `${key}=${value}`)
.join('\n')
// Check if running from command-line
if (process.argv[1].split('/').pop() === 'myJsFilename') {
...run code
}
// Single action: process.argv -> name/value collection
const ARGUMENTS = ['languageId:1', 'otherArgument']
if ((process.argv.length - 2) < ARGUMENTS.length) {
console.log('Usage: node tasks/myNodeApp ' + ARGUMENTS.map(str => `[${str.split(':')[0]}]`).join(' '))
console.log(' E.g: node tasks/myNodeApp ' + ARGUMENTS.map(str => str.split(':')[1] || '“something”').join(' '))
} else {
const argumentObj = process.argv.slice(2).reduce((result, value, index) => ({ ...result, [ARGUMENTS[index] ? ARGUMENTS[index].split(':')[0] : `arg${index + 1}`]: value }), {})
myNodeApp(argumentObj)
}
// Multiple actions: process.argv -> value array
if (process.argv[1].split('/').pop() === 'myNodeApp') {
if ((process.argv.length - 2) < 1) {
console.log('Usage: node tasks/myNodeApp [action]')
console.log(' E.g: node tasks/myNodeApp search "Topic"')
console.log(' E.g: node tasks/myNodeApp add "Name" "URL" technology,business')
console.log(' E.g: node tasks/myNodeApp import')
} else {
const cliFunctions = {
add: cliAddSource,
import: cliAddSourcesFromJSON,
search: cliSearchNewSources
}
runDatabaseFunction((pool) => cliFunctions[process.argv[2]](pool, process.argv.slice(3)))
}
}
// process.argv = ['node', 'yourscript.js', ...]
// First custom argument is 2
const NR_OF_ARGUMENTS_REQUIRED = 2
if ((process.argv.length - 2) < NR_OF_ARGUMENTS_REQUIRED) {
console.log('Usage: node myNodeApp [filename] [JSON key]')
console.log(' E.g: node myNodeApp data/test.json projects.562e3d6dfd53820c00e98bd7')
}
else {
myNodeApp(process.argv)
}
// process.argv -> two arrays of files/options
var processCommandLineArguments = function () {
var result = { files: [], options: [] }
for (var i = 1; i < process.argv.length; i++) {
if (process.argv[i][0] === '-') {
result.options.push(process.argv[i].substr(1))
}
else {
result.files.push(process.argv[i])
}
}
return result
}
// text = await getTextFromFile(filename)
const getTextFromFile = (filename) => require('fs').promises.readFile(filename, 'utf8')
// Read one line at a time / cb = (line) => {}
const getTextFromFileOneLineAtATime = async function (filename, cbLine, cbClose) {
const lineReader = require('readline').createInterface({
input: require('fs').createReadStream(filename)
})
lineReader.on('line', cbLine)
lineReader.on('close', cbClose)
}
const openFile = function (filename, cb) {
const fs = require('fs')
fs.readFile(filename, 'utf8', function (err, data) {
if (err) {
throw err
}
else if (cb) {
console.log('OK: ' + filename)
cb(data)
}
})
}
const fs = require('fs')
const fsReadFileAsync = (filePath) => new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf-8', (err, text) => {
if (err) reject(err)
else resolve(text)
})
})
const fsWriteFileAsync = (filePath, text) => new Promise((resolve, reject) => {
fs.writeFile(filePath, text, 'utf-8', (err) => {
if (err) reject(err)
else resolve(text)
})
})
const fsDeleteFileAsync = (filePath) => new Promise((resolve, reject) => {
fs.unlink(filePath, (err) => {
if (err) reject(err)
else resolve(true)
})
})
// List files in folder
const { promises: fs } = require('fs')
const fileNames = await fs.readdir('path/to/dir')
// Make folder
fs.mkdirSync(dir, { recursive: true })
// Does file exist
if (fs.existsSync('foo.txt')) {
// ...
}
import { parse } from 'csv-parse/sync'
const getCsvDataFromUrl = async function (url) {
const dataAsCsv = await fetch(url).then(res => res.text())
const dataAsArray = parse(dataAsCsv, { columns: true, skip_empty_lines: true })
return dataAsArray
}
Older:
const fs = require('fs')
const path = require('path')
const parse = require('csv-parse')
const parseCsvFile = function (fileName, actionFunction) {
const filePath = path.join(__dirname, fileName)
const parser = parse({ delimiter: ',' }, actionFunction)
fs.createReadStream(filePath).pipe(parser)
}
// Simple CSV parser: [['Name','Age'], ['Tom',45]]
const csvRows = csvText.split('\n').map(row => row.split(','))
// parseCSV: own implementation
type CsvDataRow = Record<string, any>
type CsvData = CsvDataRow[]
export const parseCSV = (text: string, separator = '\t'): CsvData => {
const rows = text.split('\n')
const headers = rows[0].split(separator)
const data = rows.slice(1).map(row => {
const values = row.split(separator)
const rowResult: CsvDataRow = {}
for (let i = 0; i < headers.length; i++) {
rowResult[headers[i]] = values[i]
}
return rowResult
})
return data
}
const csvToString = (array: Record<string, string>[], separator = '\t') => {
let str = '';
const fieldNames = Object.keys(array[0]);
const headerRow = fieldNames.join(separator);
str += headerRow + '\n';
array.forEach((row) => {
const dataRow = fieldNames.map((fieldName) => row[fieldName]).join(separator);
str += dataRow + '\n';
});
return str;
};
- Enable both HTTPS and HTTP, otherwise 301.
- Override cache control timing, if you want.
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable, stale-while-revalidate')
(Time in seconds)
Best practice: server-side encrypted cookie
https://www.npmjs.com/package/express-session https://www.npmjs.com/package/cookie-session
Domain-wide cookie:
Set-Cookie: =; Domain=.topdomain.com
See LoveBot
Twitter: need Callback URL in settings but it’s overridden in code.
NPMs: cookie-session, passport, passport-twitter etc
Init: // express-session is only in RAM, can’t do server restarts server.use(cookieSession({ name: 'myappname', keys: ['secret'], maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days })) server.use(passport.initialize()) server.use(passport.session())
Routes: server.get('/login/twitter', passport.authenticate('twitter')) server.get('/login/twitter/return', passport.authenticate('twitter', { successRedirect: '/', failureRedirect: '/?loginFailed=true' }))
Access user: req.session, req.sessionID twittername = _.get(req, 'session.passport.user.username')
More info: https://medium.com/@franxyzxyz/setting-up-free-https-with-heroku-ssl-and-lets-encrypt-80cf6eac108e
Steps:
brew install certbot
sudo certbot certonly --manual NOTE: use full domain incl "www."
Set up challenge/response on server server.get('/.well-known/acme-challenge/*', (req, res) => res.send('LONGWEIRDSTRING'))
Files end up in in /etc/letsencrypt/live/MYDOMAIN/
privkey.pem
: the private key for your certificate.
fullchain.pem
: the certificate file used in most server software.
chain.pem
: used for OCSP stapling in Nginx >=1.3.7.
cert.pem
: will break many server configurations, and should not be used without reading further documentation (see link below).
sudo heroku certs:add /etc/letsencrypt/live/MYDOMAIN/fullchain.pem /etc/letsencrypt/live/MYDOMAIN/privkey.pem Need Hobby or Professional dynos
https://github.com/bojand/mailgun-js
MAILGUN_DOMAIN=mg.wrux.it
const mailgun = require('mailgun-js')({
apiKey: process.env.MAILGUN_KEY,
domain: process.env.MAILGUN_DOMAIN,
host: 'api.eu.mailgun.net',
testMode: false
})
curl --user 'api:key-3ax6xnjp29jd6fds4gc373sgvjxteol0' \
https://api.mailgun.net/v3/samples.mailgun.org/messages \
-F from='Excited User <excited@samples.mailgun.org>' \
-F to='devs@mailgun.net' \
-F subject='Hello' \
-F text='Testing some Mailgun awesomeness!'
Node.js:
const emailData = {
from,
to,
cc,
bcc,
subject,
html,
text,
'h:Reply-To',
'o:tag': [],
'v:my-variable': '123'
}
mailgun.messages().send(emailData, (error, body) => console.log({ error, body }))
const getEmailPreview = emailData => `------------------------------
from: ${emailData.from} | reply-to: ${emailData['h:Reply-To']}
to: ${emailData.to}
bcc: ${emailData.bcc}
tags: [${emailData['o:tag'].join(', ')}]
------------------------------
subject: ${emailData.subject}
------------------------------
${emailData.html.replace(/<br\/>/g, '\n')}`
const getCurl = (emailData, mailgunConfig) => `------------------------------
curl --user 'api:${mailgunConfig.apiKey}' \\
https://${mailgunConfig.host}/v3/${mailgunConfig.domain}/messages \\
--form from='${emailData.from}' \\
--form to='curl <${emailData.to}>' \\
--form subject='${emailData.subject} (curl)' \\
--form-string html='${emailData.html}'
------------------------------`
const fetch = require('node-fetch')
const btoa = require('btoa')
const { URLSearchParams } = require('url')
const createFormBody = params => {
const urlParams = new URLSearchParams()
const keys = Object.keys(params)
for (const k in keys) {
console.log(` - ${keys[k]}: ${params[keys[k]]}`)
urlParams.append(keys[k], params[keys[k]])
}
return urlParams
}
const mailgunSendMessage = async (emailData, mailgunConfig) => {
try {
const result = await fetch(`https://${mailgunConfig.host}/v3/${mailgunConfig.domain}/messages`, {
method: 'POST',
headers: {
Authorization: 'Basic ' + btoa('api:' + mailgunConfig.apiKey)
},
body: createFormBody(emailData)
})
console.log('result:', result)
} catch (err) {
console.warn(`Warning: ${err.message || err}`)
}
}
const mailgunQuery = { event: 'stored', limit: 300 }
const body = await mailgun.events().get(mailgunQuery) const results = body.items.map(item => )
// These won't work?
// const messageId = results[i].key
// const message = await mailgun.messages(messageId).info((err, body) => console.log({err, body}))
// const message = await mailgun.get(/${config.mailgunDomain}/messages/${messageId}
)
// const message = await mailgun.get(results[i].url)
const fetchEmail = (emailUrl, method = 'GET') => fetch(emailUrl, {
method,
headers: { Authorization: 'Basic ' + Buffer.from('api' + ':' + process.env.MAILGUN_KEY).toString('base64') }
})
const deleteReceivedEmail = messageUrl => fetchEmail(messageUrl, 'DELETE') // mailgun.messages(messageId).delete()
/* Using Twilio SendGrid's v3 Node.js Library: https://github.com/sendgrid/sendgrid-nodejs
Usage:
import { sendEmailWithTemplate, EmailTemplate } from '~/services.server/email'
await sendEmailWithTemplate(EmailTemplate.GenericEmail, emailAddress, { firstName, subject, text });
Manual mode:
await sendEmail({ to, subject, text/html });
*/
import sendgridEmail, { MailDataRequired } from '@sendgrid/mail';
sendgridEmail.setApiKey(process.env.SENDGRID_API_KEY!);
const DEFAULT_EMAIL_SENDER = process.env.EMAIL_SENDER ?? 'platform@climateaction.agency';
type EmailMessageData = Omit<MailDataRequired, 'from'> & {
to: string;
subject: string;
text?: string;
html?: string;
};
export async function sendEmail(messageData: EmailMessageData) {
const newMessage = {
from: DEFAULT_EMAIL_SENDER,
...messageData,
};
const [response] = await sendgridEmail.send(newMessage as MailDataRequired);
return response;
}
/* ----- Templates: https://mc.sendgrid.com/dynamic-templates ----- */
// Inside the template editor, use {{variable}} for plain text, and triple {{{variable}}} for HTML markup
export enum EmailTemplate {
GenericEmail = 'd-de3300ebee2c45aea13abc690017e47d', // Variables: { firstName, subject, html }
}
type DynamicTemplateData = Record<string, string | number>;
export async function sendEmailWithTemplate(
templateId: EmailTemplate,
to: string,
templateVariables: DynamicTemplateData,
) {
const newMessage = {
to,
templateId: templateId,
dynamicTemplateData: templateVariables,
};
return await sendEmail(newMessage as EmailMessageData);
}
const urlRegex = require('url-regex')
const addUrlParameters = (url, newParameters) => url.includes('http')
? url.includes('?')
? `${url}&${newParameters}`
: `${url}?${newParameters}`
: url
const addLinkTracking = (html, person) => {
const utmParameters = `utm_medium=email&utm_source=cabal&utm_custom[email]=${person.email}&utm_custom[name]=${encodeURIComponent(person.name)}`
return html.replace(urlRegex({ strict: false }), url => addUrlParameters(url, utmParameters))
}
const onProcessDeath = require('death')
onProcessDeath(function (signal, err) {
// clean up code here
})
Error: error:0308010C:digital envelope routines::unsupported {
opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ],
library: 'digital envelope routines',
reason: 'unsupported',
code: 'ERR_OSSL_EVP_UNSUPPORTED'
}
Solution:
export NODE_OPTIONS=--openssl-legacy-provider
package.json:
"build": "NODE_OPTIONS=--openssl-legacy-provider next build",