Update to latest @extollo/lib & add contact API
This commit is contained in:
parent
155886eb39
commit
ac1d221f38
@ -1,4 +1,4 @@
|
|||||||
FROM node:16
|
FROM node:18
|
||||||
|
|
||||||
RUN yarn global add pnpm
|
RUN yarn global add pnpm
|
||||||
|
|
||||||
|
11
ex
11
ex
@ -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
|
||||||
|
17
package.json
17
package.json
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1416
pnpm-lock.yaml
1416
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
23
src/app/http/middlewares/RateLimit.middleware.ts
Normal file
23
src/app/http/middlewares/RateLimit.middleware.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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"],
|
||||||
|
"preserveSymlinks": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"reactNamespace": "JSX"
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"]
|
||||||
"exclude": ["node_modules", "src/app/resources", "../extollo/lib"]
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user