Skip to content

Commit

Permalink
feat(next): ssr support (#363)
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie authored Jul 21, 2022
1 parent 406082d commit 886e260
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 28 deletions.
5 changes: 4 additions & 1 deletion packages/next-sample/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ const Home = () => {
<h2>Protected resource:</h2>
<div>{protectedResource.data}</div>
<h3>
<Link href="/protected">Example: Require sign in and auto redirect</Link>
<Link href="/protected">Example1: Require sign in and auto redirect</Link>
</h3>
<h3>
<Link href="/profile-ssr">Example2: Server-render page with getServerSideProps</Link>
</h3>
</div>
)}
Expand Down
64 changes: 64 additions & 0 deletions packages/next-sample/pages/profile-ssr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { LogtoUser } from '@logto/next';
import { useMemo } from 'react';

import { logtoClient } from '../libraries/logto';

type Props = {
user: LogtoUser;
};

const ProfileSsr = ({ user }: Props) => {
const userInfo = useMemo(() => {
if (!user.isAuthenticated || !user.claims) {
return null;
}

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

return (
<div>
<header>
<h1>Hello Logto.</h1>
</header>
{userInfo}
</div>
);
};

export default ProfileSsr;

export const getServerSideProps = logtoClient.withLogtoSsr(async function ({ req, res }) {
const { user } = req;

if (!user.isAuthenticated) {
res.setHeader('location', '/api/login');
// eslint-disable-next-line @silverhand/fp/no-mutation
res.statusCode = 302;
res.end();
}

return {
props: { user },
};
});
69 changes: 45 additions & 24 deletions packages/next/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IncomingMessage } from 'http';

import NodeClient from '@logto/node';
import { withIronSessionApiRoute } from 'iron-session/next';
import { NextApiHandler, NextApiRequest } from 'next';
import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next';
import { GetServerSidePropsContext, GetServerSidePropsResult, NextApiHandler } from 'next';

import NextStorage from './storage';
import { LogtoNextConfig, LogtoUser } from './types';
Expand All @@ -24,29 +26,29 @@ export default class LogtoClient {
constructor(private readonly config: LogtoNextConfig) {}

handleSignIn = (redirectUri = `${this.config.baseUrl}/api/sign-in-callback`): NextApiHandler =>
this.withIronSession(async (request, response) => {
withIronSessionApiRoute(async (request, response) => {
const nodeClient = this.createNodeClient(request);
await nodeClient.signIn(redirectUri);
await this.storage?.save();

if (this.navigateUrl) {
response.redirect(this.navigateUrl);
}
});
}, this.ironSessionConfigs);

handleSignInCallback = (redirectTo = this.config.baseUrl): NextApiHandler =>
this.withIronSession(async (request, response) => {
withIronSessionApiRoute(async (request, response) => {
const nodeClient = this.createNodeClient(request);

if (request.url) {
await nodeClient.handleSignInCallback(`${this.config.baseUrl}${request.url}`);
await this.storage?.save();
response.redirect(redirectTo);
}
});
}, this.ironSessionConfigs);

handleSignOut = (redirectUri = this.config.baseUrl): NextApiHandler =>
this.withIronSession(async (request, response) => {
withIronSessionApiRoute(async (request, response) => {
const nodeClient = this.createNodeClient(request);
await nodeClient.signOut(redirectUri);

Expand All @@ -56,31 +58,37 @@ export default class LogtoClient {
if (this.navigateUrl) {
response.redirect(this.navigateUrl);
}
});
}, this.ironSessionConfigs);

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

withLogtoApiRoute = (handler: NextApiHandler): NextApiHandler =>
this.withIronSession(async (request, response) => {
const nodeClient = this.createNodeClient(request);
const isAuthenticated = await checkIsAuthenticatedByAccessToken(nodeClient);
await this.storage?.save();

const user: LogtoUser = {
isAuthenticated,
claims: isAuthenticated ? nodeClient.getIdTokenClaims() : undefined,
};
withIronSessionApiRoute(async (request, response) => {
const user = await this.getLogtoUserFromRequest(request);

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

return handler(request, response);
});
}, this.ironSessionConfigs);

withLogtoSsr = <P extends Record<string, unknown> = Record<string, unknown>>(
handler: (
context: GetServerSidePropsContext
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
) =>
withIronSessionSsr(async (context) => {
const user = await this.getLogtoUserFromRequest(context.req);
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
Object.defineProperty(context.req, 'user', { enumerable: true, get: () => user });

private createNodeClient(request: NextApiRequest) {
return handler(context);
}, this.ironSessionConfigs);

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

return new NodeClient(
Expand All @@ -97,14 +105,27 @@ export default class LogtoClient {
);
}

private withIronSession(handler: NextApiHandler) {
return withIronSessionApiRoute(handler, {
private get ironSessionConfigs() {
return {
cookieName: `logto:${this.config.appId}`,
password: this.config.cookieSecret,
cookieOptions: {
secure: this.config.cookieSecure,
maxAge: 14 * 24 * 60 * 60,
},
});
};
}

private async getLogtoUserFromRequest(request: IncomingMessage) {
const nodeClient = this.createNodeClient(request);
const isAuthenticated = await checkIsAuthenticatedByAccessToken(nodeClient);
await this.storage?.save();

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

return user;
}
}
6 changes: 3 additions & 3 deletions packages/next/src/storage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Storage, StorageKey } from '@logto/node';
import { IncomingMessage } from 'http';

import { NextRequestWithIronSession } from './types';
import { Storage, StorageKey } from '@logto/node';

export default class NextStorage implements Storage {
private sessionChanged = false;
constructor(private readonly request: NextRequestWithIronSession) {}
constructor(private readonly request: IncomingMessage) {}

async setItem(key: StorageKey, value: string) {
this.request.session[key] = value;
Expand Down

0 comments on commit 886e260

Please sign in to comment.