@proerd/nextpress
TypeScript icon, indicating that this package has built-in type declarations

2.3.0-dev1 • Public • Published

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
  • 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.jsons 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 the PREFIX_ prefix, envfile is read from prefix_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 route
  • createJsonRouter: express router for json apis, common middleware already included
  • createJsonRouterFromDict 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 an user 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 with init()

  • 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 the title and content props.
      • Create a route for the password reset form (default at /auth/password-reset-form). This route receives the requestId prop.
  • Auth workflow (underlying methods for the routes, in case one wants to override them)

    1. create() creates an unvalidated user and a validation token, sends validation email. Validation may be disabled (see opts).
    2. validate() validates an user with the provided hash
    3. find() looks up an user given email and password. Fails on unvalidated user
    4. createResetPwdRequest() creates the request, sends the email.
    5. findResetPwdRequest() finds if request exists but no additional side effect
    6. performResetPwd()

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

Readme

Keywords

none

Package Sidebar

Install

npm i @proerd/nextpress

Weekly Downloads

102

Version

2.3.0-dev1

License

none

Unpacked Size

698 kB

Total Files

138

Last publish

Collaborators

  • wkrueger128