Skip to content

Commit

Permalink
feat(next): with logto api route (#355)
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie authored Jul 20, 2022
1 parent 80c9e34 commit 60eb143
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 7 deletions.
14 changes: 14 additions & 0 deletions packages/next-sample/libraries/fetch-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default async function fetchJson<JSON = unknown>(
input: RequestInfo,
init?: RequestInit
): Promise<JSON> {
const response = await fetch(input, init);

const data = (await response.json()) as JSON;

if (response.ok) {
return data;
}

throw new Error(response.statusText);
}
3 changes: 2 additions & 1 deletion packages/next-sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"@logto/next": "^1.0.0-alpha.2",
"next": "^12.2.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"swr": "^1.3.0"
},
"devDependencies": {
"@silverhand/eslint-config": "^0.14.0",
Expand Down
21 changes: 21 additions & 0 deletions packages/next-sample/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AppProps } from 'next/app';
import { SWRConfig } from 'swr';

import fetchJson from '../libraries/fetch-json';

const MyApp = ({ Component, pageProps }: AppProps) => {
return (
<SWRConfig
value={{
fetcher: fetchJson,
onError: (error) => {
console.error(error);
},
}}
>
<Component {...pageProps} />
</SWRConfig>
);
};

export default MyApp;
13 changes: 13 additions & 0 deletions packages/next-sample/pages/api/protected-resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { logtoClient } from '../../libraries/logto';

export default logtoClient.withLogtoApiRoute((request, response) => {
if (!request.user.isAuthenticated) {
response.status(401).json({ message: 'Unauthorized' });

return;
}

response.json({
data: 'this_is_protected_resource',
});
});
3 changes: 3 additions & 0 deletions packages/next-sample/pages/api/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { logtoClient } from '../../libraries/logto';

export default logtoClient.handleUser();
40 changes: 37 additions & 3 deletions packages/next-sample/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,46 @@
import { LogtoUser } from '@logto/next';
import Link from 'next/link';
import useSWR from 'swr';

const Home = () => {
const { data } = useSWR<LogtoUser>('/api/user');
const { data: protectedResource } = useSWR<{ data: string }>('/api/protected-resource');

if (!data) {
return null;
}

return (
<div>
Hello Logto.{' '}
<Link href="/api/sign-in">
<a>Sign In</a>
</Link>
{data.isAuthenticated ? (
<Link href="/api/sign-out">
<a>Sign Out</a>
</Link>
) : (
<Link href="/api/sign-in">
<a>Sign In</a>
</Link>
)}
{data.isAuthenticated && data.claims && (
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{Object.entries(data.claims).map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
)}
<div>{protectedResource?.data}</div>
</div>
);
};
Expand Down
3 changes: 3 additions & 0 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
"ts-jest": "^27.0.4",
"typescript": "^4.5.5"
},
"peerDependencies": {
"next": ">=12"
},
"eslintConfig": {
"extends": "@silverhand"
},
Expand Down
21 changes: 21 additions & 0 deletions packages/next/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const getItem = jest.fn();
const save = jest.fn();
const signIn = jest.fn();
const handleSignInCallback = jest.fn();
const getIdTokenClaims = jest.fn(() => ({
sub: 'user_id',
}));

jest.mock('./storage', () =>
jest.fn(() => ({
Expand All @@ -43,6 +46,8 @@ jest.mock('@logto/node', () =>
signIn();
},
handleSignInCallback,
isAuthenticated: true,
getIdTokenClaims,
}))
);

Expand Down Expand Up @@ -88,4 +93,20 @@ describe('Next', () => {
expect(save).toHaveBeenCalled();
});
});

describe('withLogtoApiRoute', () => {
it('should assign `user` to `request`', async () => {
const client = new LogtoClient(configs);
await testApiHandler({
handler: client.withLogtoApiRoute((request, response) => {
expect(request.user).toBeDefined();
response.end();
}),
test: async ({ fetch }) => {
await fetch({ method: 'GET', redirect: 'manual' });
},
});
expect(getIdTokenClaims).toHaveBeenCalled();
});
});
});
32 changes: 30 additions & 2 deletions packages/next/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import NodeClient from '@logto/node';
import { withIronSessionApiRoute } from 'iron-session/next';
import { NextApiHandler, NextApiRequest } from 'next';
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';

import NextStorage from './storage';
import { LogtoNextConfig } from './types';
import { LogtoNextConfig, LogtoUser, NextApiRequestWithLogtoUser } from './types';

export type { LogtoUser } from './types';

export default class LogtoClient {
private navigateUrl?: string;
Expand Down Expand Up @@ -32,6 +34,32 @@ export default class LogtoClient {
}
});

handleUser = () =>
this.withLogtoApiRoute((request, response) => {
response.json(request.user);
});

withLogtoApiRoute = (
handler: (
request: NextApiRequestWithLogtoUser,
response: NextApiResponse
) => unknown | Promise<unknown>
): NextApiHandler =>
this.withIronSession(async (request, response) => {
const nodeClient = this.createNodeClient(request);
const { isAuthenticated } = nodeClient;

const user: LogtoUser = {
isAuthenticated,
claims: isAuthenticated ? nodeClient.getIdTokenClaims() : undefined,
};

// eslint-disable-next-line @silverhand/fp/no-mutating-methods
Object.defineProperty(request, 'user', { enumerable: true, get: () => user });

return handler(request as NextApiRequestWithLogtoUser, response);
});

private createNodeClient(request: NextApiRequest) {
this.storage = new NextStorage(request);

Expand Down
11 changes: 10 additions & 1 deletion packages/next/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LogtoConfig } from '@logto/node';
import { IdTokenClaims, LogtoConfig } from '@logto/node';
import { IronSession } from 'iron-session';
import { NextApiRequest } from 'next';

Expand All @@ -18,3 +18,12 @@ export type LogtoNextConfig = LogtoConfig & {
cookieSecure: boolean;
baseUrl: string;
};

export type LogtoUser = {
isAuthenticated: boolean;
claims?: IdTokenClaims;
};

export type NextApiRequestWithLogtoUser = NextApiRequest & {
user: LogtoUser;
};
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 60eb143

Please sign in to comment.