Exobase hook function that provides authentication by way of validating the x-api-key
header.
yarn add @exobase/use-api-key
# or
yarn add @exobase/hooks
import { useApiKey } from '@exobase/use-api-key'
// or
import { useApiKey } from '@exobase/hooks'
You can use useApiKey
to secure an endpoint with a single static key.
import { compose } from 'radash'
import type { Props } from '@exobase/core'
import { useNext } from '@exobase/use-next'
import { useApiKey } from '@exobase/use-api-key'
export const securePingEndpoint = async (props: Props) => ({
message: 'pong'
})
const SECRET_KEY = 'only-my-friends-know-this'
export default compose(useNext(), useApiKey(SECRET_KEY), securePingEndpoint)
If you need to validate the provided api key depending on the request you can pass an async function that will receive the props.
import { compose } from 'radash'
import type { Props } from '@exobase/core'
import { useNext } from '@exobase/use-next'
import { useServices, useApiKey } from '@exobase/hooks'
import makeDatabase, { Database } from './database'
export const securePingEndpoint = async ({
args,
services
}: Props<Args, Services>) => {
return {
message: 'pong'
}
}
const SECRET_KEY = 'only-my-friends-know-this'
export default compose(
useNext(),
useServices({
db: makeDatabase
}),
useApiKey(async ({ services, request }) => {
const { db } = services
const clientId = request.headers['x-client-id']
const client = await db.clients.find(clientId)
return client?.apiKey
}),
securePingEndpoint
)
If you need to use this type of workflow, a few recomendations
-
To make sure you don't have to do the
db.clients.find
call more than once, use a custom hook to do the query and then save the client to theProps
so any function in the composition can access. -
So you don't have to copy the code in each function, compose the
useApiKey
into a custom hook that can be called without arguments.
You can write this in your project, I like to keep them in a /hooks
directory. The hook will do the database lookup and then append the client
to the Args
in the Props
.
import { partial } from 'radash'
import type { Database } from './database'
type Args = {}
type Services = {
db: Database
}
const withClientArg = (func: Handler, props: Props<Args, Services>) => {
const { db } = props.services
const clientId = request.headers['x-client-id']
const client = await db.clients.find(clientId)
return await func({
...props,
args: {
...props.args,
client
}
})
}
export const useClientArg = () => (func: Handler) => {
return partial(withClientArg, func)
}
We can now use our useClientArg
hook in a function.
export default compose(
useNext(),
useServices({
db: makeDatabase
}),
useClientArg(),
useApiKey(({ args }) => args.client?.apiKey),
securePingEndpoint
)
That alone is much better, but we can still improve things.
We'll create another custom hook, this one is much much simpler
import { useApiKey } from '@exobase/hooks'
import type { Client } from './types'
type Args = {
client: Client
}
export const useClientApiKey = () =>
useApiKey(({ args }: Props<Args>) => args.client?.apiKey)
Now we can use it in our functions.
export default compose(
useNext(),
useServices({
db: makeDatabase
}),
useClientArg(),
useClientApiKey(),
securePingEndpoint
)
Personally, I don't recommend this. I think what we have now is a perfect balance of DRY and abstracted. But, so you know it's possible and incase you prefer it, I'll show you how to compose all this into a useClientAuth
hook.
We'll create a new hook called useClientAuth
where we'll wrap everything we need, start to finish, to authenticate a request of this type.
export const useClientAuth = () =>
compose(
useServices({
db: makeDatabase
}),
useClientArg(),
useClientApiKey()
)
Then we can use it in our function.
export default compose(useNext(), useClientAuth(), securePingEndpoint)