Update to latest @extollo/lib & add contact API

This commit is contained in:
Garrett Mills 2022-11-14 19:49:34 -06:00
parent 155886eb39
commit ac1d221f38
11 changed files with 1010 additions and 575 deletions

View File

@ -1,4 +1,4 @@
FROM node:16 FROM node:18
RUN yarn global add pnpm RUN yarn global add pnpm

11
ex
View File

@ -135,14 +135,5 @@ if [ ! -d "./node_modules" ]; then
echo "" echo ""
printf "\033[32m✓\033[39m Looks like you're all set up! Run this command again to access the Extollo CLI.\n" printf "\033[32m✓\033[39m Looks like you're all set up! Run this command again to access the Extollo CLI.\n"
else else
start_spinner "Building your app..." "$ENV_PNPM" cli "$@"
BUILD_OUTPUT="$($ENV_PNPM run build 2>&1)"
BUILD_EC=$?
stop_spinner $BUILD_EC
if [ $BUILD_EC -ne 0 ]; then
printf "\033[31m✘\033[39m Uh, oh! Looks like your application failed to build. (exit: $BUILD_EC)"
echo "$BUILD_OUTPUT"
exit $BUILD_EC
fi
"$ENV_NODE" --experimental-repl-await ./lib/cli.js $@
fi fi

View File

@ -8,13 +8,15 @@
"lib": "lib" "lib": "lib"
}, },
"dependencies": { "dependencies": {
"@extollo/lib": "^0.10.5", "@atao60/fse-cli": "^0.1.7",
"@extollo/lib": "^0.14.8",
"@types/node": "^18.11.9",
"any-date-parser": "^1.5.3", "any-date-parser": "^1.5.3",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"feed": "^4.2.2", "feed": "^4.2.2",
"gotify": "^1.1.0", "gotify": "^1.1.0",
"rimraf": "^3.0.2",
"ts-expose-internals": "^4.5.4", "ts-expose-internals": "^4.5.4",
"ts-node": "^10.9.1",
"ts-patch": "^2.0.1", "ts-patch": "^2.0.1",
"ts-to-zod": "^1.8.0", "ts-to-zod": "^1.8.0",
"typescript": "^4.3.2", "typescript": "^4.3.2",
@ -22,9 +24,11 @@
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build": "excc -c package.json -t tsconfig.json", "build": "pnpm run clean && tsc -p tsconfig.json && fse copy --all --dereference --preserveTimestamps --keepExisting=false --quiet --errorOnExist=false src/app/resources lib/app/resources",
"app": "pnpm run build && node lib/index.js", "clean": "rimraf lib",
"cli": "pnpm run build && node lib/cli.js", "watch": "nodemon --ext js,pug,ts --watch src --exec 'ts-node src/index.ts'",
"app": "ts-node src/index.ts",
"cli": "ts-node src/cli.ts",
"docker:build": "docker build -t ${DOCKER_REGISTRY}/garrettmills/www .", "docker:build": "docker build -t ${DOCKER_REGISTRY}/garrettmills/www .",
"docker:push": "docker push ${DOCKER_REGISTRY}/garrettmills/www" "docker:push": "docker push ${DOCKER_REGISTRY}/garrettmills/www"
}, },
@ -51,6 +55,7 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@extollo/cc": "^0.6.0" "@extollo/cc": "^0.6.0",
"rimraf": "^3.0.2"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ import {
file, file,
Application, Application,
make, make,
Valid, Logging, Valid, Logging, api,
} from '@extollo/lib' } from '@extollo/lib'
import {WorkItem} from '../../models/WorkItem.model' import {WorkItem} from '../../models/WorkItem.model'
import {FeedPost} from '../../models/FeedPost.model' import {FeedPost} from '../../models/FeedPost.model'
@ -111,13 +111,21 @@ export class Home extends Controller {
.read() .read()
} }
async contact(data: Valid<ContactForm>) { async contact(data: ContactForm) {
// If the request has an "e-mail" field, then this was likely filled out by a spam await this.sendContact(data)
// bot, as this field is hidden on the form. So, reject it. return view('message', {
if ( this.request.input('e-mail') ) { title: 'Message Sent',
data.name = `SPAM: ${data.name}` // for testing, just alter the name message: 'Your message has been sent. Thanks! I\'ll be in touch soon.',
} buttonAction: this.routing.getNamedPath('home').toRemote,
})
}
async contactApi(data: ContactForm) {
await this.sendContact(data)
return api.one({})
}
protected async sendContact(data: ContactForm) {
const submission = make<ContactSubmission>(ContactSubmission) const submission = make<ContactSubmission>(ContactSubmission)
submission.name = data.name submission.name = data.name
submission.email = data.email submission.email = data.email
@ -135,11 +143,6 @@ export class Home extends Controller {
data.message, data.message,
].join('\n'), ].join('\n'),
}).then(x => this.logging.debug(x)) }).then(x => this.logging.debug(x))
.catch(e => this.logging.error(e))
return view('message', {
title: 'Message Sent',
message: 'Your message has been sent. Thanks! I\'ll be in touch soon.',
buttonAction: this.routing.getNamedPath('home').toRemote,
})
} }
} }

View File

@ -14,7 +14,7 @@ import {
Maybe, Maybe,
QueryRow, QueryRow,
} from '@extollo/lib' } from '@extollo/lib'
import {FieldDefinition, FieldType, ResourceAction, ResourceConfiguration} from '../../../cobalt' import {FieldDefinition, FieldType, ResourceAction, ResourceConfiguration, SelectOptions} from '../../../cobalt'
const parser = require('any-date-parser') const parser = require('any-date-parser')
@ -197,7 +197,7 @@ export class ResourceAPI extends Controller {
} }
return cast return cast
} else if ( type === FieldType.select && hasOwnProperty(fieldDef, 'options') ) { } else if ( type === FieldType.select && hasOwnProperty(fieldDef, 'options') ) {
const options = collect(fieldDef.options) const options = collect(fieldDef.options as SelectOptions)
if ( options.pluck('value').includes(value) ) { if ( options.pluck('value').includes(value) ) {
return value return value
} }

View File

@ -0,0 +1,23 @@
import {Cache, http, HTTPStatus, Inject, Injectable, Middleware} from '@extollo/lib'
/**
* RateLimit Middleware
* --------------------------------------------
* Limits a route to one request / 30 seconds / IP address.
*/
@Injectable()
export class RateLimit extends Middleware {
@Inject()
protected readonly cache!: Cache
public async apply() {
const slug = `extollo__rate_limit__${this.request.path}__${this.request.address.address}`
if ( await this.cache.has(slug) ) {
return http(HTTPStatus.TOO_MANY_REQUESTS)
}
const date = new Date()
date.setSeconds(date.getSeconds() + 30) // one request / 30 seconds
await this.cache.put(slug, slug, date)
}
}

View File

@ -1,4 +1,14 @@
import {ParameterMiddleware, Injectable, Either, ResponseObject, Validator, Valid, right} from '@extollo/lib' import {
Either, hasOwnProperty,
http,
HTTPStatus,
Inject,
Injectable,
left, Logging,
ParameterMiddleware,
ResponseObject,
right,
} from '@extollo/lib'
import {ContactForm} from '../../../types/ContactForm.type' import {ContactForm} from '../../../types/ContactForm.type'
/** /**
@ -7,9 +17,20 @@ import {ContactForm} from '../../../types/ContactForm.type'
* Parse the contact form data and validate it. Provide the fields as middleware. * Parse the contact form data and validate it. Provide the fields as middleware.
*/ */
@Injectable() @Injectable()
export class ValidContactForm extends ParameterMiddleware<Valid<ContactForm>> { export class ValidContactForm extends ParameterMiddleware<ContactForm> {
async handle(): Promise<Either<ResponseObject, Valid<ContactForm>>> { @Inject()
const validator = new Validator<ContactForm>() protected readonly logging!: Logging
return right(validator.parse(this.request.input()))
async handle(): Promise<Either<ResponseObject, ContactForm>> {
const allInput = this.request.input()
if ( typeof allInput !== 'object' || allInput === null || !hasOwnProperty(allInput, 'e-mail') || allInput['e-mail'] ) {
return left(http(HTTPStatus.UNAUTHORIZED))
}
return right({
email: this.request.safe('email').present().string(),
name: this.request.safe('name').present().string(),
message: this.request.safe('message').present().string(),
})
} }
} }

View File

@ -7,6 +7,7 @@ import {Feed} from '../controllers/Feed.controller'
import {LoadSnippet} from '../middlewares/parameters/LoadSnippet.middleware' import {LoadSnippet} from '../middlewares/parameters/LoadSnippet.middleware'
import {LoadFeedPosts} from '../middlewares/parameters/LoadFeedPosts.middleware' import {LoadFeedPosts} from '../middlewares/parameters/LoadFeedPosts.middleware'
import {ValidContactForm} from '../middlewares/parameters/ValidContactForm.middleware' import {ValidContactForm} from '../middlewares/parameters/ValidContactForm.middleware'
import {RateLimit} from '../middlewares/RateLimit.middleware'
Route Route
.group('/', () => { .group('/', () => {
@ -19,10 +20,17 @@ Route
.handledBy(() => redirect('/blog/')) .handledBy(() => redirect('/blog/'))
Route.post('/contact') Route.post('/contact')
.pre(RateLimit)
.parameterMiddleware(ValidContactForm) .parameterMiddleware(ValidContactForm)
.calls<Home>(Home, home => home.contact) .calls<Home>(Home, home => home.contact)
.alias('contact') .alias('contact')
Route.post('/api/contact')
.pre(RateLimit)
.parameterMiddleware(ValidContactForm)
.calls<Home>(Home, home => home.contactApi)
.alias('contact-api')
Route.get('/humans.txt') Route.get('/humans.txt')
.calls<Home>(Home, home => home.humans) .calls<Home>(Home, home => home.humans)

View File

@ -1,4 +1,4 @@
import {Application, CommandLineApplication} from '@extollo/lib' import {Application, CommandLineApplication, Foreground} from '@extollo/lib'
import {Units} from './Units.extollo' import {Units} from './Units.extollo'
/* /*
@ -22,12 +22,7 @@ export function cli(): Application {
const app = Application.getApplication() const app = Application.getApplication()
app.forceStartupMessage = false app.forceStartupMessage = false
const units = [...Units] const units = [...Units, CommandLineApplication]
units.reverse()
CommandLineApplication.setReplacement(units[0])
units[0] = CommandLineApplication
units.reverse()
app.scaffold(__dirname, units) app.scaffold(__dirname, units)
return app return app
@ -38,6 +33,10 @@ export function cli(): Application {
*/ */
export function app(): Application { export function app(): Application {
const app = Application.getApplication() const app = Application.getApplication()
app.scaffold(__dirname, Units)
const units = [...Units]
units.push(Foreground)
app.scaffold(__dirname, units)
return app return app
} }

View File

@ -1,14 +1,17 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6", "target": "esnext",
"module": "commonjs", "module": "commonjs",
"declaration": true, "declaration": true,
"outDir": "./lib", "outDir": "./lib",
"strict": true, "strict": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"lib": ["ESNext"] "skipLibCheck": true,
}, "lib": ["esnext", "dom", "dom.iterable"],
"include": ["src"], "preserveSymlinks": true,
"exclude": ["node_modules", "src/app/resources", "../extollo/lib"] "jsx": "react",
"reactNamespace": "JSX"
},
"include": ["src"]
} }