DenisFrezzato / hyper-ts

Type safe middleware architecture for HTTP servers
https://denisfrezzato.github.io/hyper-ts/
MIT License
391 stars 18 forks source link

export unsafeResponseStateTransition + authentication example #5

Closed gcanti closed 6 years ago

gcanti commented 6 years ago

@OliverJAsh @sledorze This is an example showing how to ensure authentication statically. I think that there are other ways to do this, more sophisticated and that can handle authorization as well, but I guess this is just fine as a first attempt

import * as express from 'express'
import {
  status,
  closeHeaders,
  send,
  MiddlewareTask,
  param,
  of,
  Handler,
  unsafeResponseStateTransition
} from 'hyper-ts/lib/MiddlewareTask'
import { Status, StatusOpen } from 'hyper-ts'
import { Option, some, none } from 'fp-ts/lib/Option'
import * as t from 'io-ts'
import * as task from 'fp-ts/lib/Task'
import { tuple } from 'fp-ts/lib/function'
import { IntegerFromString } from 'io-ts-types/lib/number/IntegerFromString'

// the new connection state
type Authenticated = 'Authenticated'

interface Authentication
  extends MiddlewareTask<StatusOpen, StatusOpen, Option<MiddlewareTask<StatusOpen, Authenticated, void>>> {}

const withAuthentication = (strategy: (req: express.Request) => task.Task<boolean>): Authentication =>
  new MiddlewareTask(c => {
    return strategy(c.req).map(authenticated => tuple(authenticated ? some(unsafeResponseStateTransition) : none, c))
  })

// dummy authentication process
const tokenAuthentication = withAuthentication(req => task.of(t.string.is(req.get('token'))))

// dummy ResponseStateTransition (like closeHeaders)
const authenticated: MiddlewareTask<Authenticated, StatusOpen, void> = unsafeResponseStateTransition

//
// error handling combinators
//

const badRequest = (message: string) =>
  status(Status.BadRequest)
    .ichain(() => closeHeaders)
    .ichain(() => send(message))

const notFound = (message: string) =>
  status(Status.NotFound)
    .ichain(() => closeHeaders)
    .ichain(() => send(message))

const unauthorized = (message: string) =>
  status(Status.Unauthorized)
    .ichain(() => closeHeaders)
    .ichain(() => send(message))

//
// user
//

interface User {
  name: string
}

// the result of this function requires a successful authentication upstream
const loadUser = (id: number) => authenticated.ichain(() => of(id === 1 ? some<User>({ name: 'Giulio' }) : none))

const getUserId = param('user_id', IntegerFromString)

const sendUser = (user: User) =>
  status(Status.OK)
    .ichain(() => closeHeaders)
    .ichain(() => send(`Hello ${user.name}!`))

const user: Handler = getUserId.ichain(oid =>
  oid.fold(
    () => badRequest('Invalid user id'),
    id =>
      tokenAuthentication.ichain(oAuthenticated =>
        oAuthenticated.fold(
          () => unauthorized('Unauthorized user'),
          authenticated =>
            authenticated.ichain(() => loadUser(id).ichain(ou => ou.fold(() => notFound('User not found'), sendUser)))
        )
      )
  )
)

const app = express()
app.get('/:user_id', user.toRequestHandler())
app.listen(3000, () => console.log('App listening on port 3000!'))
sledorze commented 6 years ago

@gcanti TBH I've not spent any time looking - yet. We're currently thinking about replacing our framework (Hapi) as our microservice basis.

Also we're using funfix as our async stack layer (for its scheduler). So I'm unsure about hyper-ts yet, I'm unsure about the execution compatibility and performance.

What we know is that we want to be able to easily integrate with existing libs (as we cannot rewrite everything from scratch) and express has a huge ecosystem.

gcanti commented 6 years ago

TBH I've not spent any time looking - yet

That's fine, this is just an experimental project at the moment (oh and sorry for mentioning you, let me know if you are not interested in further discussions)

Also we're using funfix as our async stack layer (for its scheduler)

I'm not acquainted with funfix, what can you do with its scheduler?

I'm unsure about the execution compatibility and performance

I would expect to be able to write an interpreter for funfix's IO as I did for fp-ts's Task. Performance is a big deal though, I'll investigate how to benchmark this library (any suggestion?).

sledorze commented 6 years ago

@gcanti thanks for keeping me in the loop!

About funfix scheduler, you can read about it here: https://github.com/funfix/funfix/pull/37 which is exhibited when chaining computations; with some load.

About performance; what need to be measured is the overhead.

Benchmarks are not reliable but the more accurate that we can be; I guess a basic 'ab' against a set of representative handlers can give some hints, compared to a baseline (ex: raw express).

Can be done in a CI context too, even if not on separate boxes, it can give some idea and compare behaviours (for different interpreters for instance)..