Skip to content
Snippets Groups Projects
Verified Commit 2c86ee7c authored by Louis's avatar Louis :fire:
Browse files

Replace old structure with Hackerfest format

parent 3b2c1557
No related branches found
No related tags found
No related merge requests found
...@@ -4,10 +4,8 @@ root = true ...@@ -4,10 +4,8 @@ root = true
end_of_line = lf end_of_line = lf
insert_final_new_line = true insert_final_new_line = true
charset = utf-8 charset = utf-8
[*.{rs, js,json,mustache,hbs,handlebars}]
indent_style = tab indent_style = tab
[*.{yml,yaml}] [*.{yaml,yml}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
\ No newline at end of file
APP_NAME=
APP_KEY= APP_KEY=
PORT=8000 APP_NAME=jetsam
PORT=10123
WEB_URL= PROXY_REQUESTS=false
PROXY_HOSTS=
DATABASE_NAME=trash API_URL=http://hack.4l2.uk/api
DATABASE_USER=trash WEB_URL=http://hack.4l2.uk/
DATABASE_PASS=trash
MAIL_DRIVER=log DATABASE_HOST=127.0.0.1
MAIL_FROM_ADDRESS= DATABASE_NAME=jetsam
MAIL_FROM_NAME= DATABASE_USER=jetsam
DATABASE_PASS=jetsam
DATABASE_PORT=5434
MAIL_DRIVER=smtp
MAIL_FROM=test@example.com
MAIL_FROM_NAME="Jetsam Tech"
MAIL_REPLY_TO=text@example.com
SMTP_HOST=smtp.mailtrap.io
SMTP_PORT=2525
SMTP_USERNAME=cde41a26e17536
SMTP_PASSWORD=2bb6527a5564d7
SENDGRID_KEY= SENDGRID_KEY=
FS_DRIVER=gcs
FILE_ROOT=./storage
FILE_URL_PATH=/api/fs
GCS_BUCKET= GCS_BUCKET=
GCS_CREDENTIALS_B64=
CACHE_DRIVER=redis
REDIS_URL=redis://127.0.0.1:25000
QUEUE_DRIVER=amqp
SENTRY_DSN= AMQP_HOST=127.0.0.1
AMQP_PORT=25001
AMQP_USER=guest
AMQP_PASSWORD=guest
AMQP_SECURE=false
SLACK_WEBHOOK= SENTRY_ENABLED=false
SENTRY_DSN=
\ No newline at end of file
SENTRY_ENABLED=false
SENTRY_SAMPLE_RATE=0
SENTRY_DSN=
module.exports = {
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": "@bark",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": 1,
"no-return-await": 0, // Common koa middleware pattern
}
};
\ No newline at end of file
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
...@@ -4,4 +4,10 @@ node_modules ...@@ -4,4 +4,10 @@ node_modules
.env .env
*.iml *.iml
**/dist/ **/dist/
public/js/
stats.json
client-legacy/
.dck/
storage/
certs/
google-storage.json google-storage.json
\ No newline at end of file
const path = require('path'); const path = require('path')
module.exports = { module.exports = {
'config': path.resolve('src', 'config', 'sequelize.js'), 'config': path.resolve('src', 'config', 'sequelize.js'),
'models-path': path.resolve('src', 'database', 'models'), 'models-path': path.resolve('src', 'database', 'models'),
'seeders-path': path.resolve('database', 'seeders'), 'seeders-path': path.resolve('database', 'seeders'),
'migrations-path': path.resolve('database', 'migrations') 'migrations-path': path.resolve('database', 'migrations'),
} }
\ No newline at end of file
const BaseModel = require('./BaseModel')
const timestamps = require('./properties/timestamps') const timestamps = require('./properties/timestamps')
class Model extends BaseModel {
}
module.exports = (sequelize, DataTypes) => { module.exports = (sequelize, DataTypes) => {
const Model = sequelize.define('', Object.assign( Model.init(Object.assign(
{ {
id: { id: {
type: DataTypes.UUID, type: DataTypes.UUID,
...@@ -17,27 +22,12 @@ module.exports = (sequelize, DataTypes) => { ...@@ -17,27 +22,12 @@ module.exports = (sequelize, DataTypes) => {
}, },
timestamps(DataTypes), timestamps(DataTypes),
), { ), {
sequelize,
paranoid: true, paranoid: true,
tableName: '', tableName: '',
}) })
Model.getPolyIdentifier = () => ''
Model.getRelationIdentifier = () => ''
Model.prototype.toJSON = function userToJSON() {
return {
id: this.id,
meta: this.meta,
created_at: this.created_at,
updated_at: this.updated_at,
}
}
Model.associate = function defineModelAssociations(models) {
}
Model.relations = []
return Model return Model
} }
\ No newline at end of file
module.exports.Model = Model
## [1.3.1] - 2020 / 09 / 04
### Changed
- fromDate is ignored for metric queries, 12 months of data will be returned
\ No newline at end of file
FROM mhart/alpine-node:12
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production --unsafe-perm
FROM mhart/alpine-node:slim-12
LABEL maintainer="Louis Capitanchik <contact@louiscap.co>"
LABEL description="A basic Node.js web app"
RUN apk add --no-cache bash
WORKDIR /app
COPY --from=0 /app/node_modules ./node_modules
COPY server.js .
COPY src ./src
# Add more COPY lines here as resources are added
ENV NODE_PATH=/app/src
ARG PORT=8000
ENV PORT ${PORT}
EXPOSE ${PORT}
CMD ["node", "server"]
\ No newline at end of file
# node-koa-starter
A full-fat, go get 'em starter that I use to build enterprise web applications.
## Contents
* [Rationale](#rationale)
* [Included Batteries](#included-batteries)
* [Docker](#docker)
* [Code Conventions](#code-conventions)
* [Dependency Injection](#dependency-injection)
* [Service Facades](#service-facades)
* [Spaces vs Tabs](#spaces-vs-tabs)
## Rationale
I build a lot of large web applications; sometimes they're monoliths, sometimes they're
a series of microservices. This starter represents the collected wisdom that I've gained over
the years on how to structure a Node.js web app so that it's usable both on the micro and macro
scales without too much churn.
The included libraries have been chosen for their utility in any situation; there isn't any point
in including an email facade when not every project would need to send emails (microservices), or
including a JSON:API response transformer (not every project would even speak REST!).
Express is an outdated framework. It has a lot of good things going for it, and the community support
is astounding, but koa is a lot tighter and provides a nicer base on which to build bespoke functionality.
The one controversial point that I will concede is the usage of `NODE_PATH` to get absolute module imports.
This may not be to everyone's taste, but forms a core basis of some of the dependency injection techniques
used.
## Included Batteries
- `koa` - The HTTP request handling framework that forms the core of this starter. Modern, slim, BYOB attitude with enough community modules to cover the bases.
- `koa-static` - Not mounted be default, but included because many projects will need to serve static assets at some point
- `koa-router` - The defacto router for Koa, supports middleware, sub-routers and name based url-generation
- `koa-bodyparser` - Parses request bodies for forms and json uploads
- `koa-logger` - Basic request logging & timing
- `koa-cors` - Handle cors `OPTIONS` requests, very customisable
- `dotenv` - Environment file based configuration, feeds into the `env`/`config` multi-layered app configuration strategy
- `moment` - Almost all apps handle dates or times (even for logging purposes)
- `moment-range` - Configured by default to allow seamless date-range manipulation
- `fs-jetpack` - Easier file system handling. A global instance is added to the `bootstrap` to provide a default fs rooted to the app directory
- `uuid` - Standard for adding IDs to things
- `lodash` - Utilities for most apps
- `core/errors` - HTTP errors that, when not caught, will set the response status and contents appropriately
## Docker
The included `Dockerfile` works by default for this setup, but will need tweaking as new resources are added to the project.
It utilises multi-stage builds to build the app in an alpine container, and then copies the resources to a slim container
for a smaller output size. Additional `COPY` statements and `ENV` configs will need to be added as required.
## Code Conventions
Application entry points are located at the root level of the project. For example, `server.js` is a server entry point. When building
additional components that make use of a shared code base (e.g. a queue worker), they would be located at the root level as well
(e.g. a `worker.js` file).
All external configuration should be stored in a file located under `src/config`. These files should either be a JSON file or a JS
file that exports an object. Using the `config` helper from the `bootstrap` will access values from these files, utilising the node
module cache to prevent multiple potentially expensive lookups (e.g. syscalls to the environment). If the config object has a `driver`
key, the `config` helper will instead use the property with a name that matches the value of `driver` when retrieving values.
e.g.
```javascript
const { env } = require('bootstrap')
module.exports = {
driver: env('DATABASE_DRIVER', 'postgres'),
postgres: {
// ... postgres config ...
},
mysql: {
// ... mysql config ...
},
}
```
External services fall in to two categories; configured SDKs and service facades. Configured SDKs should export a configured version
of their parent-library, or an SDK factory, from `src/vendor/{library-name}` such that another module can `require('vendor/mylib')` to
use that library. An example of this would be using `pg`; `src/vendor/pg` would export a configured `pg` instance, typically using
`config` or `env` from the `bootstrap` to pull in the configuration.
Service facades are detailed more below, but essentially take the form of a configured mapping of a specific vendor service to a
generic service interface. For example, the `cache` service included with this starter has the `null` and `memory` implementations,
which are switched between by the `CACHE_DRIVER` environment variable. A `redis` implementation could be added by mapping an `ioredis`
configured SDK (detailed above) to a `redis` service that implements the `cache` facade.
Dependency injection is detailed more below, but functions by implementing the `ContextualModule` class and then adding that class
to the `ServiceProvider` module. Each `ContextualModule` declares it's exported name, and will be made available to any entity that
has access to the koa `ctx` object.
## Dependency Injection
- TBW
## Service Facades
- TBW
## Spaces Vs Tabs
The project is configured to expect tab-based indentation for the following reasons:
- Accessibility: I am dyslexic and I can configure the width of a tab to render as a comfortable amount of space to make understanding code easier
- Consistency: Every line has X number of tabs based on bracket nesting. There are no spaces for further alignment and the linting rules prevent mixed indentation so it's not possible to succumb to the temptation of vanity indentation
- Preference: I simply prefer tabs. There are arguments for either, and I don't care too deeply about it. Tabs give me the things I need, so I use them.
\ No newline at end of file
version: '3.3'
services:
redis:
image: 'redis:6'
ports:
- '25000:6379'
volumes:
- ./.dck/redis:/data
labels:
uk.co.hackerfest.environment: 'staging'
amqp:
image: 'rabbitmq:3-management'
hostname: jetsam_rabbit
ports:
- '25001:5672'
- '25002:15672'
volumes:
- ./.dck/rabbit:/data
labels:
uk.co.hackerfest.environment: 'staging'
\ No newline at end of file
This diff is collapsed.
{ {
"name": "jetsam-server", "name": "jetsam-api",
"version": "1.3.1", "version": "2.0.0-beta.1",
"description": "", "description": "The Jetsam App API Server",
"main": "index.js", "main": "server.js",
"scripts": { "scripts": {
"test": "NODE_PATH=src jest --passWithNoTests=true", "watch": "NODE_PATH=src DEBUG=server:* nodemon server --ignore './client/src' --ignore './certs' --ignore 'google-storage.json'",
"start": "NODE_PATH=src node server", "watch:queue": "NODE_PATH=src QUEUE_ACTION=consumer DEBUG=server:* nodemon worker --ignore './client/src' --ignore './certs' --ignore 'google-storage.json'",
"watch": "NODE_PATH=src nodemon server --ignore './client/src/' --ignore './client/node_modules'", "exec:env": "docker-compose -p jetenv up",
"sql": "NODE_PATH=src sequelize", "test": "NODE_ENV=testing NODE_PATH=src node scripts/jest.js",
"repl": "NODE_PATH=src node -e 'Object.entries(require(\"bootstrap\")).forEach(([key, value]) => Object.defineProperty(global, key, { value }))' -i" "start": "NODE_PATH=src node server",
}, "cmd": "NODE_PATH=src node run",
"keywords": [], "sql": "NODE_PATH=src node scripts/npx-boot.js sequelize",
"author": "", "repl": "NODE_PATH=src node -e 'Object.entries(require(\"bootstrap\")).forEach(([key, value]) => Object.defineProperty(global, key, { value }))' -i"
"license": "ISC", },
"dependencies": { "author": "Louis Capitanchik <louis@microhacks.co.uk>",
"@google-cloud/storage": "^4.1.3", "license": "GPL-3.0+",
"@koa/multer": "^2.0.2", "dependencies": {
"@sendgrid/mail": "^6.5.4", "@google-cloud/storage": "^5.5.0",
"@sentry/node": "^5.12.2", "@koa/cors": "^3.1.0",
"defer-class": "^1.0.1", "@koa/multer": "^3.0.0",
"dotenv": "^8.1.0", "@koa/router": "^9.3.1",
"fs-jetpack": "^2.2.2", "@sentry/node": "^6.1.0",
"handlebars": "^4.5.3", "@sentry/tracing": "^6.1.0",
"koa": "^2.8.1", "amqplib": "^0.6.0",
"koa-bodyparser": "^4.2.1", "change-case": "^4.1.1",
"koa-cors": "0.0.16", "dataloader": "^2.0.0",
"koa-logger": "^3.2.1", "debug": "^4.2.0",
"koa-oauth-server": "^1.0.1", "dotenv": "^8.2.0",
"koa-router": "^7.4.0", "dotenv-expand": "^5.1.0",
"koa-session": "^5.12.3", "fs-jetpack": "^2.4.0",
"koa-static": "^5.0.0", "handlebars": "^4.7.6",
"lodash": "^4.17.15", "ioredis": "^4.17.3",
"mime-types": "^2.1.26", "joi": "^17.3.0",
"moment": "^2.24.0", "koa": "^2.13.0",
"moment-range": "^4.0.2", "koa-bodyparser": "^4.3.0",
"multer": "^1.4.2", "koa-compress": "^5.0.1",
"node-fetch": "^2.6.0", "koa-csrf": "^3.0.8",
"oauth2-server": "^3.0.1", "koa-etag": "^3.0.0",
"pg": "^7.12.1", "koa-logger": "^3.2.1",
"scrypt-kdf": "^2.0.1", "koa-mount": "^4.0.0",
"sequelize": "^5.21.2", "koa-session": "^6.0.0",
"strip-ansi": "^6.0.0", "koa-static": "^5.0.0",
"uuid": "^3.3.2" "lodash": "^4.17.19",
}, "mime-types": "^2.1.27",
"devDependencies": { "moment": "^2.27.0",
"@bark/eslint-config": "^0.1.2", "moment-range": "^4.0.2",
"eslint": "^6.2.0", "multer": "^1.4.2",
"jest": "^24.9.0", "nodemailer": "^6.4.17",
"nodemon": "^1.19.1", "oauth2-server": "^3.1.1",
"sequelize-cli": "^5.5.1", "pg": "^8.3.0",
"supertest": "^4.0.2" "pg-hstore": "^2.3.3",
} "pluralize": "^8.0.0",
"redbird": "^0.10.0",
"remarkable": "^2.0.1",
"request": "^2.88.2",
"request-promise-native": "^1.0.8",
"scrypt-kdf": "^2.0.1",
"sequelize": "^6.3.3",
"sequelize-cli": "^6.2.0",
"twittersignin": "^1.2.0",
"uuid": "^8.3.1",
"yargs": "^13.3.2"
},
"devDependencies": {
"jest": "^26.6.3",
"nodemon": "^2.0.4",
"supertest": "^6.1.3"
}
} }
* {
box-sizing: border-box;
position: relative;
}
.required {
color: red;
}
.logo {
border-radius: 10px;
box-shadow: 0px 10px 30px 0 #BBB;
margin: 0 5rem;
}
.header {
padding: 3rem;
display: flex;
justify-content: center;
}
.error-message {
padding: 1rem;
margin: 1rem 0;
text-align: center;
background-color: #FF4136;
border: 1px solid #FF2015;
color: white;
font-weight: bold;
}
button:not(:disabled).button-primary {
background-color: #FF4D75;
border-color: #FF4D75;
background: linear-gradient(to right, #FF7BAC, #FF1E3D);
}
button:not(:disabled).button-primary:hover {
background-color: #FF5E82;
border-color: #FF5E82;
background: linear-gradient(to right, #ff88b4, #ff3450);
}
button:disabled {
background-color: #777;
border-color: #777;
}
button:disabled:hover {
background-color: #777;
border-color: #777;
}
.flex-row {
padding: 2rem 0;
display: flex;
flex-direction: row;
align-items: center;
}
.flex-row h4 {
margin: 0;
}
.centered {
text-align: center;
}
a {
color: #FF5E82;
}
a:hover {
color: #ff3450;
}
.back-icon {
margin-right: 1rem;
}
@media screen and (max-width: 760px) {
.header {
flex-direction: column;
text-align: center;
align-items: center;
}
.logo {
margin-bottom: 5rem;
}
}
input[type=text]:disabled {
background: #FFF5F5;
}
input[type=text].error,
input[type=password].error {
border-color: red;
}
\ No newline at end of file
(function () {
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('reset_password_form')
const tokenField = document.getElementById('reset_token')
const newField = document.getElementById('new_password')
const confirmField = document.getElementById('confirm_password')
const messageField = document.getElementById('formmessage')
function validateInput() {
const newValue = newField.value
const confirmValue = confirmField.value
if (newValue !== confirmValue) {
messageField.innerText = 'The passwords do not match'
confirmField.classList.toggle('error', true)
return false
} else {
messageField.innerText = ''
confirmField.classList.toggle('error', false)
return true
}
}
newField.addEventListener('input', validateInput)
confirmField.addEventListener('input', validateInput)
form.addEventListener('submit', async function (e) {
e.preventDefault()
if (!validateInput()) {
return
}
const payload = {
reset_token: tokenField.value,
new_password: newField.value,
confirm_password: confirmField.value,
}
const result = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(payload),
})
if (!result.ok && result.status !== 400) {
messageField.innerText = 'There was a problem resetting your password. Please request a new password reset link and try again later.'
return
}
if (!result.ok && result.status === 400) {
const data = await result.clone().json()
const { error } = data
messageField.innerText = error.message
return
}
const parent = form.parentElement
parent.removeChild(form)
const success = document.createElement('div')
success.innerHTML = `<h3 class="centered">Password Reset Successful</h3>
<p class="centered">You successfully reset your password. You can log in to the Jetsam app with the password you just created. Now go and snap some pesky plastics!</p>
<div class="row centered">
\t<a href="https://jetsam.tech">Go to the Jetsam website</a>
</div>`
parent.appendChild(success)
})
})
}())
\ No newline at end of file
run.js 0 → 100644
const cluster = require('cluster')
process.env.NODE_PATH = 'src'
async function runWorker() {
const yargs = require('yargs')
const bootstrap = require('bootstrap')
await bootstrap.boot()
yargs(process.argv.slice(2))
.scriptName('cmd')
.commandDir(bootstrap.fs.path('src', 'console'))
.demandCommand()
.recommendCommands()
.help()
.argv
}
async function runMaster() {
const bootstrap = require('bootstrap')
await bootstrap.boot()
let resolve, reject
const defer = new Promise((res, rej) => {
resolve = res
reject = rej
})
const child = cluster.fork({
NODE_PATH: 'src',
...process.env,
})
child.once('error', reject)
child.on('exit', resolve)
await defer
}
async function run() {
if (cluster.isWorker) {
await runMaster()
} else {
await runWorker()
}
}
run()
.catch(e => {
console.error(e)
process.exit(1)
})
\ No newline at end of file
const { execSync } = require('child_process')
const bootstrap = require('../src/bootstrap')
const { v4: uuid } = require('uuid')
const { exec } = require('core/utils/process')
async function run() {
process.env.NODE_ENV = 'testing'
await bootstrap.boot()
const id = uuid().replace(/-/g, '')
const dbname = `${ bootstrap.config('database.database') }_${ id }`
await bootstrap.invoke('db:fresh', [id, '--and-migrate'], false)
try {
const runner = await exec('npx jest --forceExit --runInBand', {
env: {
...process.env,
DATABASE_NAME: dbname,
SENTRY_ENABLED: false,
},
stdio: 'inherit',
}, true)
} finally {
await bootstrap.invoke('db:prune')
process.exit()
}
}
run()
.catch(e => {
console.error(e)
process.exit(1)
})
const child_process = require('child_process')
const bootstrap = require('../src/bootstrap')
async function run() {
await bootstrap.boot()
if (process.argv.length > 2) {
const [_r, _p, command, ...args] = process.argv
child_process.execSync(`npm run ${ command } ${ args.join(' ') }`, {
env: process.env,
stdio: 'inherit',
})
}
}
run()
\ No newline at end of file
const child_process = require('child_process')
const bootstrap = require('../src/bootstrap')
async function run() {
await bootstrap.boot()
if (process.argv.length > 2) {
const [_r, _p, command, ...args] = process.argv
child_process.execSync(`npx ${ command } ${ args.join(' ') }`, {
env: process.env,
stdio: 'inherit',
})
}
}
run()
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment