@proerd/nextpress
v2.3.0-dev1
Published
Package website things that could become commmon between projects into a module.
Downloads
73
Readme
nextpress
Package website things that could become commmon between projects into a module.
Trying not to worry much about config options, it is of intention to have one big opinionated monolythic package.
Currently bundling:
- dotenv
- express
- next.js as a view layer
- a default configs setup with
dotenv
- some scaffolding (I'd like less but huh)
- DB support: knex/redis
- sessions or jwts
- an auth workflow
- front-end reacty common things (react, react-dom, redux, formik...)
- moved to
nextpress-client
package
- moved to
- jest
- with typescript in mind
Limitations (FIXMEs)
- Design for a coupled "monolithic" small server (API and website in the same project, not necessarily in the same script)
scaffolding
yarn add @proerd/nextpress
ps: we rely upon yarn, npm client is not much tested and may not work due to node_modules folder structure differences.
Add to your package.json:
{
"scripts": {
"nextpress": "nextpress"
}
}
Invoke:
yarn run nextpress --scaffold
There will be two tsconfig.json
s around. The one on the root is invoked by next.js when you start the server. The one inside the server
folder needs to be manually built.
On VSCode: F1 > Run build task > Watch at server/tsconfig.json.
Server (compiled) will be available at .nextpress/<file>.js
. The first time you run it it may complain something and create an envfile.env
which you should edit. The required variables depend on which defaultContexts
are added on server/index.ts
.
WEBSITE_ROOT="http://localhost:8080"
WEBSITE_PORT=8080
WEBSITE_SESSION_SECRET=iamsecret
If you don't want it to create an envfile (ex: netlify, heroku), set NO_ENVFILE=1
. Required envvar check still takes place.
Folder structure goes like this:
|_ .next (next.js things)
|_ .nextpress (the compiled server)
|_ app (put your client-side here)
|_ pages (next.js suggested "pages" folder for router entries)
|_ server
|_ static
| ...
How to extend things
Some things here are presented as classes with a bunch of replaceable functions.
Tested/Recommended:
const auth = new UserAuth(ctx)
auth.sendMail = myImplementation
Untested:
class CustomUserAuth {
sendMail = myImplementation
}
const auth = new CustomUserAuth(ctx)
How to require things
- The root require is now empty
- Require through
@proerd/nextpress/lib/<modulename>
Context tool
Reads from the envfile and populates a settings object which is meant to be used throughout the project.
May also provide (singleton-ish) methods which use the related env settings.
import { ContextFactory } from "@nextpress/context"
import { websiteContext } from "@nextpress/context/mappers/website"
const context = ContextFactory({
mappers: [
websiteContext,
{
id: "auction_scan",
envKeys: ["BNET_API_KEY"],
optionalKeys: ["RUN_SERVICE"],
envContext: ({ getKey }) => ({
apiKey: getKey("BNET_API_KEY")!,
runService: Boolean(getKey("RUN_SERVICE")),
}),
},
],
projectRoot: path.resolve(__dirname, ".."),
})
A "context mapper" describes the mapping from the env
keys to the resulting object. A couple of default defaultMappers
are provided. Select which you need to use for the project.
Prefixes
A different context file and set may be used by adding a prefix
.
Ex: with prefix=
prefix_
. Expected envvars get thePREFIX_
prefix, envfile is read fromprefix_envfile.env
.
Custom mappers must use getKey
(from example above) to support prefixes.
Extending the typedefs
The context type is globally defined in Nextpress.Context
, and shall be declaration-merged through Nextpress.CustomContext
when necessary. See the default contexts implementation for examples.
declare global {
namespace Nextpress {
interface CustomContext {
newKeys: goHere
}
}
}
"The context imports are verbose!" It wasnt always like this, but writing it this way allows for declaration merging (dynamic Nextpress.Context type) which pays it off. You may make use of the VSCode "auto imports" to reduce keystrokes there.
Website context
Required by most other things here.
Website root on the client
While on the server the website root path can be easily acessed through the context, on the client process.env.WEBSITE_ROOT
is used (it is replaced on the build stage -- see the .babelrc
for details).
Bundle analyzer
- Turn on with
WEBSITE_BUNDLE_ANALYZER
env option
Knex context
ctx.knex.db()
gets a knex instance;- optional
ctx.database.init({ currentVersion, migration })
contains a helper regarding table creation and migrations. - You still have to install the database driver (default is
mysql
)
Mailgun context
Redis context
- Requires installing
ioredis
peer dependency.
Default webpack config
Currently includes:
- Typescript
- CSS (no modules)
- Sass (no modules)
- Lodash plugin (reduce bundle size, this effects even if you are not directly using lodash)
- Bundle analyzer runs if
WEBSITE_BUNDLE_ANALYZER
is provided
Override it by replacing the corresponding Server#getNextJsConfig
method.
dev vs. production
If starting with NODE_ENV = production
, the server runs the equivalent of next build
, then next start
.
Server
The scaffold comes with something like:
import { Server, ContextFactory } from "nextpress"
const context = ContextFactory(...)
const server = new Server(context)
server.run()
Server
expects its context to have the website
default mapper. It already bundles session middleware, looking for the following contexts to use as stores:
- Redis
- Knex
- Fallbacks to dev-mode in-memory session
server
has an OOPish interceptor pattern, you may set it up by overriding its available methods.
//default route setup
async setupRoutes({ app }: { app: ExpressApp }): Promise<void> {
const builder = new RouterBuilder(this)
app.use(await builder.createHtmlRouter())
}
Adding routes must be done inside setupRoutes
. Use RouterBuilder
for a couple of predefined templates. See signatures while using the editor.
createHtmlRouter
: create an express router with includes next.js, and etcetera. Next.js is already included on the end of the stack, additional routes you write are added BEFORE the next.js routecreateJsonRouter
: express router for json apis, common middleware already includedcreateJsonRouterFromDict
offers an opinionated approach for setting up routes.- static helper methods:
tryMw
,appendJsonRoutesFromDict
- Overrideable
jsonErrorHandler
The the createRouter
methids RETURN a router, you still has to write app.use(router)
to bind it to the main express instance.
Cut-down sample:
const server = new Server(ctx)
server.routeSetup = async app => {
const routerBuilder = new RouterBuilder(server)
const { tryMw } = RouterBuilder
const htmlRouter = await routerBuilder.createHtmlRouter(async ({ router }) => {
router.get(
"/",
tryMw((req, res) => {
if (!req.session!.user) {
return server.getNextApp().render(req, res, "/unauth")
}
return res.redirect("/dashboard")
}),
)
//...
})
app.use(htmlRouter)
const api = setup.createJsonRouterFromDict(router, helpers => {
"/createuser": async req => {
await User.create({
email: req.body.newUserEmail,
password: req.body.newUserPwd,
})
return { status: "OK" }
},
})
app.use("/api", api)
}
server.run()
Auth boilerplate
Dependencies:
- You need to install the
bcrypt
peer dependency in order to use this; - This initially looks for
knex
for storing the user data. If the knex context is not present, you'd need to suppy another implementation in.userStore
. - This looks for a
ctx.email.sendMail
key for sending mail. Default mailgun context has that, or you could supply another context with the same key.
import UserAuth from "@proerd/nextpress/lib/server/user-auth"
const userAuth = new UserAuth(ctx)
await userAuth.init()
Contains common session auth workflow things.
This is shaped as an OOPish interceptor pattern with a bunch of extension points. Override methods to customize things.
init()
This creates anuser
table in the database;routineCleanup()
is meant to be manually added to some scheduled job (hourly expected), cleans up unvalidated users and unused password reset requests. It also is ran along withinit()
JSON routes
throwOnUnauthMw
(to be used on express routes behind an auth/session gate)userRoutes(opts).json
generates an express router with a set of routes from the workflow methods (all them POST + JSON). You are supposed to create your auth forms then AJAX-invoke these./createUser
{ newUserEmail, newUserPwd }
/login
{ username, password }
/request-password-reset
{ email }
/perform-password-reset
{ pwd1, pwd2, requestId }
/logout
userRoutes(opts).html
generates preset next.js (html+GET) routes which are called back from e-mails:/auth/validate?seq=<hash>
redirects to a success message (see below)/auth/forgotPassword?seq=
redirects to a custom form (see below)
Required additional setup:
- Create a route for displaying simple messages (default path
/auth/message.tsx
), this receives thetitle
andcontent
props. - Create a route for the password reset form (default at
/auth/password-reset-form
). This route receives therequestId
prop.
- Create a route for displaying simple messages (default path
Auth workflow (underlying methods for the routes, in case one wants to override them)
create()
creates an unvalidated user and a validation token, sends validation email. Validation may be disabled (see opts).validate()
validates an user with the provided hashfind()
looks up an user given email and password. Fails on unvalidated usercreateResetPwdRequest()
creates the request, sends the email.findResetPwdRequest()
finds if request exists but no additional side effectperformResetPwd()
Behavior: By default, auth requests are capped at 10 reqs / 10 sec (most) and 30 reqs / 10 sec (login). Each email may see 1 login attempt every 15 seconds. Overrideables to change that are:
_userRequestCap
_getRequestThrottleMws
OBS: As login is persisted on session, nexts Router.push
won't work after logging in. A full page reload is required.
TODO
all of the rest