Skip to content

Commit

Permalink
feat(express,next,node): support fetchUserInfo
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie committed Sep 29, 2022
1 parent 1de579a commit 4885cc9
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 12 deletions.
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('/user', withLogto(config), (request, response) => {
response.json(request.user);
});

app.get('/protected', requireAuth, (request, response) => {
app.get(
'/user-fetched',
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 });
34 changes: 32 additions & 2 deletions packages/next-sample/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import useSWR from 'swr';

const Home = () => {
const { data } = useSWR<LogtoContext>('/api/logto/user');
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 +36,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 +80,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();
});
});
});
12 changes: 11 additions & 1 deletion packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default class LogtoClient extends BaseClient {
});
}

getContext = async (getAccessToken = false): Promise<LogtoContext> => {
getContext = async (getAccessToken = false, fetchUserInfo = false): Promise<LogtoContext> => {
const isAuthenticated = await this.isAuthenticated();

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

Expand All @@ -79,6 +80,7 @@ export default class LogtoClient extends BaseClient {
return {
isAuthenticated,
claims: await this.getIdTokenClaims(),
userInfo: await this._fetchUserInfo(fetchUserInfo),
accessToken,
};
} catch {
Expand All @@ -87,4 +89,12 @@ export default class LogtoClient extends BaseClient {
};
}
};

private readonly _fetchUserInfo = async (fetch: boolean) => {
if (!fetch) {
return;
}

return this.fetchUserInfo();
};
}
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;
};

0 comments on commit 4885cc9

Please sign in to comment.