Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(express,next,node): support fetchUserInfo #413

Merged
merged 1 commit into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions packages/express-sample/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const app = express();
app.use(cookieParser());
app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 14 * 24 * 60 * 60 } }));
app.use(handleAuthRoutes(config));
app.use(withLogto(config));

app.get('/', (request, response) => {
response.setHeader('content-type', 'text/html');
Expand All @@ -37,11 +36,23 @@ app.get('/', (request, response) => {
);
});

app.get('/user', (request, response) => {
app.get('/local-user-claims', withLogto(config), (request, response) => {
response.json(request.user);
});

app.get('/protected', requireAuth, (request, response) => {
app.get(
'/remote-full-user',
withLogto({
...config,
// Fetch user info from remote, this may slowdown the response time, not recommended.
fetchUserInfo: true,
}),
(request, response) => {
response.json(request.user);
}
);

app.get('/protected', withLogto(config), requireAuth, (request, response) => {
response.end('protected resource');
});

Expand Down
2 changes: 1 addition & 1 deletion packages/express/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const withLogto =
(config: LogtoExpressConfig): Middleware =>
async (request: IncomingMessage, response: Response, next: NextFunction) => {
const client = createNodeClient(request, response, config);
const user = await client.getContext(config.getAccessToken);
const user = await client.getContext(config.getAccessToken, config.fetchUserInfo);
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
Object.defineProperty(request, 'user', { enumerable: true, get: () => user });
next();
Expand Down
1 change: 1 addition & 0 deletions packages/express/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ declare module 'http' {
export type LogtoExpressConfig = LogtoConfig & {
baseUrl: string;
getAccessToken?: boolean;
fetchUserInfo?: boolean;
};
3 changes: 3 additions & 0 deletions packages/next-sample/pages/api/logto/user-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { logtoClient } from '../../../libraries/logto';

export default logtoClient.handleUser({ fetchUserInfo: true });
36 changes: 34 additions & 2 deletions packages/next-sample/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ import { useMemo } from 'react';
import useSWR from 'swr';

const Home = () => {
// Use server's id token claims
const { data } = useSWR<LogtoContext>('/api/logto/user');
// Remote full user info
const { data: dataWithUserInfo } = useSWR<LogtoContext>('/api/logto/user-info');
const { data: protectedResource } = useSWR<{ data: string }>('/api/protected-resource');

const userInfo = useMemo(() => {
const claims = useMemo(() => {
if (!data?.isAuthenticated || !data.claims) {
return null;
}

return (
<div>
<h2>User info:</h2>
<h2>Claims:</h2>
<table>
<thead>
<tr>
Expand All @@ -35,6 +38,34 @@ const Home = () => {
);
}, [data]);

const userInfo = useMemo(() => {
if (!dataWithUserInfo?.isAuthenticated || !dataWithUserInfo.userInfo) {
return null;
}

return (
<div>
<h2>User info:</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{Object.entries(dataWithUserInfo.userInfo).map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>{JSON.stringify(value)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}, [dataWithUserInfo]);

return (
<div>
<header>
Expand All @@ -51,6 +82,7 @@ const Home = () => {
</Link>
)}
</nav>
{claims}
{userInfo}
{protectedResource && (
<div>
Expand Down
14 changes: 11 additions & 3 deletions packages/next/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ export default class LogtoClient {

withLogtoApiRoute = (handler: NextApiHandler, config: WithLogtoConfig = {}): NextApiHandler =>
withIronSessionApiRoute(async (request, response) => {
const user = await this.getLogtoUserFromRequest(request, config.getAccessToken);
const user = await this.getLogtoUserFromRequest(
request,
config.getAccessToken,
config.fetchUserInfo
);

// eslint-disable-next-line @silverhand/fp/no-mutating-methods
Object.defineProperty(request, 'user', { enumerable: true, get: () => user });
Expand Down Expand Up @@ -128,9 +132,13 @@ export default class LogtoClient {
};
}

private async getLogtoUserFromRequest(request: IncomingMessage, getAccessToken?: boolean) {
private async getLogtoUserFromRequest(
request: IncomingMessage,
getAccessToken?: boolean,
fetchUserInfo?: boolean
) {
const nodeClient = this.createNodeClient(request);

return nodeClient.getContext(getAccessToken);
return nodeClient.getContext(getAccessToken, fetchUserInfo);
}
}
1 change: 1 addition & 0 deletions packages/next/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ export type LogtoNextConfig = LogtoConfig & {
*/
export type WithLogtoConfig = {
getAccessToken?: boolean;
fetchUserInfo?: boolean;
};
15 changes: 14 additions & 1 deletion packages/node/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const storage = {
};

const getAccessToken = jest.fn(async () => true);
const fetchUserInfo = jest.fn(async () => ({ name: 'name' }));
const getIdTokenClaims = jest.fn(async () => ({ sub: 'sub' }));
const isAuthenticated = jest.fn(async () => true);
jest.mock('@logto/client', () => ({
Expand All @@ -19,6 +20,7 @@ jest.mock('@logto/client', () => ({
getAccessToken,
getIdTokenClaims,
isAuthenticated,
fetchUserInfo,
})),
createRequester: jest.fn(),
}));
Expand All @@ -33,6 +35,7 @@ describe('LogtoClient', () => {
describe('getContext', () => {
beforeEach(() => {
getAccessToken.mockClear();
fetchUserInfo.mockClear();
});

it('should set isAuthenticated to false when "getAccessToken" is enabled and is unable to getAccessToken', async () => {
Expand All @@ -42,14 +45,24 @@ describe('LogtoClient', () => {
expect(getAccessToken).toHaveBeenCalled();
});

it('should return context and not call getAccessToken by default', async () => {
it('should fetch remote user info and return when "fetchUserInfo" is enabled', async () => {
const client = new LogtoClient({ endpoint, appId }, { navigate, storage });
await expect(client.getContext(false, true)).resolves.toMatchObject({
claims: { sub: 'sub' },
userInfo: { name: 'name' },
});
expect(fetchUserInfo).toHaveBeenCalled();
});

it('should return context and not call getAccessToken and fetchUserInfo by default', async () => {
const client = new LogtoClient({ endpoint, appId }, { navigate, storage });
await expect(client.getContext()).resolves.toEqual({
isAuthenticated: true,
claims: { sub: 'sub' },
});
expect(getIdTokenClaims).toHaveBeenCalled();
expect(getAccessToken).not.toHaveBeenCalled();
expect(fetchUserInfo).not.toHaveBeenCalled();
});
});
});
7 changes: 6 additions & 1 deletion packages/node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import BaseClient, { LogtoConfig, createRequester, ClientAdapter } from '@logto/client';
import { conditional } from '@silverhand/essentials';
import fetch from 'node-fetch';

import { LogtoContext } from './types';
Expand Down Expand Up @@ -55,7 +56,8 @@ export default class LogtoClient extends BaseClient {
});
}

getContext = async (getAccessToken = false): Promise<LogtoContext> => {
/* eslint-disable complexity */
getContext = async (getAccessToken = false, fetchUserInfo = false): Promise<LogtoContext> => {
const isAuthenticated = await this.isAuthenticated();

if (!isAuthenticated) {
Expand All @@ -70,6 +72,7 @@ export default class LogtoClient extends BaseClient {
return {
isAuthenticated,
claims,
userInfo: conditional(fetchUserInfo && (await this.fetchUserInfo())),
};
}

Expand All @@ -79,6 +82,7 @@ export default class LogtoClient extends BaseClient {
return {
isAuthenticated,
claims: await this.getIdTokenClaims(),
userInfo: conditional(fetchUserInfo && (await this.fetchUserInfo())),
accessToken,
};
} catch {
Expand All @@ -87,4 +91,5 @@ export default class LogtoClient extends BaseClient {
};
}
};
/* eslint-enable complexity */
}
3 changes: 2 additions & 1 deletion packages/node/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IdTokenClaims } from '@logto/client';
import { IdTokenClaims, UserInfoResponse } from '@logto/client';

declare module 'http' {
// Honor module definition
Expand All @@ -12,4 +12,5 @@ export type LogtoContext = {
isAuthenticated: boolean;
claims?: IdTokenClaims;
accessToken?: string;
userInfo?: UserInfoResponse;
};