Skip to content

Commit

Permalink
document auth (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
KATT committed Mar 1, 2021
1 parent b3d4b9f commit 8009db5
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 18 deletions.
105 changes: 104 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ tRPC is a framework for building strongly typed RPC APIs with TypeScript. Altern
- [Merging routers](#merging-routers)
- [Router middlewares](#router-middlewares)
- [Data transformers](#data-transformers)
- [Authorization](#authorization)
- [Server-side rendering (SSR / SSG)](#server-side-rendering-ssr--ssg)
- [Using `ssr.prefetchOnServer()` (recommended)](#using-ssrprefetchonserver-recommended)
- [Invoking directly](#invoking-directly)
Expand Down Expand Up @@ -111,7 +112,7 @@ import * as trpc from '@trpc/server';
import * as trpcNext from '@trpc/server/dist/adapters/next';
import * as z from 'zod';

// The app's context - is typically generated for each request
// The app's context - is generated for each incoming request
export type Context = {};
const createContext = ({
req,
Expand Down Expand Up @@ -407,6 +408,108 @@ You are able to serialize the response data & input args (in order to be able to
- `createNextApiHandler()` in [`./examples/next-ssg-chat/[...trpc.ts]`](./examples/next-ssg-chat/pages/api/trpc/%5B...trpc%5D.ts), and
- `createTRPCClient` in [`./examples/next-ssg-chat/pages/_app.tsx`](./examples/next-ssg-chat/pages/_app.tsx)

## Authorization

The `createContext`-function is called for each incoming request so here you can add contextual information about the calling user from the request object.

<details><summary>Create context from request headers</summary>

```ts
import * as trpc from '@trpc/server';
import { inferAsyncReturnType } from '@trpc/server';
import { decodeAndVerifyJwtToken } from './somewhere/in/your/app/utils';

export async function createContext({
req,
res,
}: trpcNext.CreateNextContextOptions) {
// Create your context based on the request object
// Will be available as `ctx` in all your resolvers

// This is just an example of something you'd might want to do in your ctx fn
async function getUserFromHeader() {
if (req.headers.authorization) {
const user = await decodeAndVerifyJwtToken(req.headers.authorization.split(' ')[1])
return user;
}
return null;
}
const user = await getUserFromHeader();

return {
user,
};
}
type Context = inferAsyncReturnType<typeof createContext>;

// [..] Define API handler and app router
```
</details>
<details><summary>Authorize using resolver</summary>

```ts
import * as trpc from '@trpc/server';
import { createRouter } from './[...trpc]';

export const appRouter = createRouter()
// open for anyone
.query('hello', {
input: z.string().optional(),
resolve: ({ input, ctx }) => {
return `hello ${input ?? ctx.user?.name ?? 'world'}`;
},
})
// checked in resolver
.query('secret', {
resolve: ({ ctx }) => {
if (!ctx.user) {
throw trpc.httpError.unauthorized();
}
if (ctx.user?.name !== 'KATT') {
throw trpc.httpError.forbidden();
}
return {
secret: 'sauce',
};
},
}),
);
```
</details>
<details><summary>Authorize using middleware</summary>

```ts
import * as trpc from '@trpc/server';
import { createRouter } from './[...trpc]';

export const appRouter = createRouter()
// this is accessible for everyone
.query('hello', {
input: z.string().optional(),
resolve: ({ input, ctx }) => {
return `hello ${input ?? ctx.user?.name ?? 'world'}`;
},
})
.merge(
'admin.',
createRouter()
// this protectes all procedures defined after in this router
.middleware(async ({ ctx }) => {
if (!ctx.user?.isAdmin) {
throw httpError.unauthorized();
}
})
.query('secret', {
resolve: ({ ctx }) => {
return {
secret: 'sauce',
}
},
}),
)
```
</details>

## Server-side rendering (SSR / SSG)

> - See the [chat example](./examples/next-ssg-chat) for a working example.
Expand Down
2 changes: 1 addition & 1 deletion examples/next-hello-world/pages/api/trpc/[...trpc].ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as trpcNext from '@trpc/server/dist/adapters/next';
import { inferAsyncReturnType } from '@trpc/server';
import { postsRouter } from './posts';

// The app's context - is typically generated for each request
// The app's context - is generated for each incoming request
export async function createContext({
req,
res,
Expand Down
26 changes: 13 additions & 13 deletions examples/playground/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ async function main() {
await client.query('hello');
await client.query('hello', 'client');
await sleep();
const postCreate = await client.mutation('posts/create', {
const postCreate = await client.mutation('posts.create', {
title: 'hello client',
});
console.log('created post', postCreate.title);
await sleep();
const postList = await client.query('posts/list');
const postList = await client.query('posts.list');
console.log('has posts', postList, 'first:', postList[0].title);
await sleep();
try {
await client.query('admin/secret');
await client.query('admin.secret');
} catch (err) {
// will fail
}
Expand All @@ -48,14 +48,14 @@ async function main() {
}),
});

await authedClient.query('admin/secret');
await authedClient.query('admin.secret');

const msgs = await client.query('messages/list');
const msgs = await client.query('messages.list');
console.log('msgs', msgs);

let i = 0;

const unsubscribe = client.subscription('posts/newMessage', {
const unsubscribe = client.subscription('posts.newMessage', {
initialInput: {
timestamp: msgs.reduce((max, msg) => Math.max(max, msg.createdAt), 0),
},
Expand All @@ -73,18 +73,18 @@ async function main() {
});

await Promise.all([
client.mutation('messages/add', `test message${i++}`),
client.mutation('messages/add', `test message${i++}`),
client.mutation('messages/add', `test message${i++}`),
client.mutation('messages/add', `test message${i++}`),
client.mutation('messages.add', `test message${i++}`),
client.mutation('messages.add', `test message${i++}`),
client.mutation('messages.add', `test message${i++}`),
client.mutation('messages.add', `test message${i++}`),
]);
await sleep();

await client.mutation('messages/add', `test message${i++}`);
await client.mutation('messages.add', `test message${i++}`);

await Promise.all([
client.mutation('messages/add', `test message${i++}`),
client.mutation('messages/add', `test message${i++}`),
client.mutation('messages.add', `test message${i++}`),
client.mutation('messages.add', `test message${i++}`),
]);

unsubscribe();
Expand Down
6 changes: 3 additions & 3 deletions examples/playground/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ export const appRouter = createRouter()
return `hello ${input ?? ctx.user?.name ?? 'world'}`;
},
})
.merge('posts/', posts)
.merge('posts.', posts)
.merge(
'admin/',
'admin.',
createRouter().query('secret', {
resolve: ({ ctx }) => {
if (!ctx.user) {
Expand All @@ -138,7 +138,7 @@ export const appRouter = createRouter()
},
}),
)
.merge('messages/', messages);
.merge('messages.', messages);

export type AppRouter = typeof appRouter;

Expand Down

0 comments on commit 8009db5

Please sign in to comment.