Skip to content

Commit cee889a

Browse files
authored
refactor(next): use cookie storage (#783)
1 parent 799dc6d commit cee889a

File tree

17 files changed

+167
-379
lines changed

17 files changed

+167
-379
lines changed
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
/** @type {import('next').NextConfig} */
2-
const nextConfig = {
3-
experimental: {
4-
serverActions: true,
5-
},
6-
};
2+
const nextConfig = {};
73

84
module.exports = nextConfig;

packages/next/edge/index.ts

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { RequestCookies, ResponseCookies } from '@edge-runtime/cookies';
2-
import { createSession, type GetContextParameters, type InteractionMode } from '@logto/node';
2+
import { CookieStorage, type GetContextParameters, type InteractionMode } from '@logto/node';
33
import NodeClient from '@logto/node/edge';
44
import { type NextRequest } from 'next/server';
55

@@ -30,7 +30,6 @@ export default class LogtoClient extends BaseClient {
3030
async (request: Request) => {
3131
const { nodeClient, headers } = await this.createNodeClientFromEdgeRequest(request);
3232
await nodeClient.signIn(redirectUri, interactionMode);
33-
await this.storage?.save();
3433

3534
const response = new Response(null, {
3635
headers,
@@ -50,7 +49,6 @@ export default class LogtoClient extends BaseClient {
5049
const { nodeClient, headers } = await this.createNodeClientFromEdgeRequest(request);
5150
await nodeClient.signOut(redirectUri);
5251
await this.storage?.destroy();
53-
await this.storage?.save();
5452

5553
const response = new Response(null, {
5654
headers,
@@ -78,7 +76,6 @@ export default class LogtoClient extends BaseClient {
7876
this.config.baseUrl
7977
);
8078
await nodeClient.handleSignInCallback(callbackUrl.toString());
81-
await this.storage?.save();
8279
}
8380

8481
const response = new Response(null, {
@@ -102,33 +99,35 @@ export default class LogtoClient extends BaseClient {
10299
getLogtoContext = async (request: NextRequest, config: GetContextParameters = {}) => {
103100
const { nodeClient } = await this.createNodeClientFromEdgeRequest(request);
104101
const context = await nodeClient.getContext(config);
105-
await this.storage?.save();
106102

107103
return context;
108104
};
109105

110106
async createNodeClientFromEdgeRequest(request: Request) {
111-
const cookieName = `logto:${this.config.appId}`;
112107
const cookies = new RequestCookies(request.headers);
113108
const headers = new Headers();
114109
const responseCookies = new ResponseCookies(headers);
115110

116-
const nodeClient = super.createNodeClient(
117-
await createSession(
118-
{
119-
secret: this.config.cookieSecret,
120-
crypto,
121-
},
122-
cookies.get(cookieName)?.value ?? '',
123-
(value) => {
124-
responseCookies.set(cookieName, value, {
125-
maxAge: 14 * 3600 * 24,
126-
secure: this.config.cookieSecure,
127-
sameSite: this.config.cookieSecure ? 'lax' : undefined,
128-
});
129-
}
130-
)
131-
);
111+
this.storage = new CookieStorage({
112+
encryptionKey: this.config.cookieSecret,
113+
cookieKey: `logto:${this.config.appId}`,
114+
isSecure: this.config.cookieSecure,
115+
getCookie: (name) => {
116+
return cookies.get(name)?.value ?? '';
117+
},
118+
setCookie: (name, value, options) => {
119+
responseCookies.set(name, value, options);
120+
},
121+
});
122+
123+
await this.storage.init();
124+
125+
const nodeClient = new this.adapters.NodeClient(this.config, {
126+
storage: this.storage,
127+
navigate: (url) => {
128+
this.navigateUrl = url;
129+
},
130+
});
132131

133132
return { nodeClient, headers };
134133
}

packages/next/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,14 @@
5757
},
5858
"dependencies": {
5959
"@edge-runtime/cookies": "^4.0.0",
60-
"@logto/node": "workspace:^"
60+
"@logto/node": "workspace:^",
61+
"cookie": "^0.6.0"
6162
},
6263
"devDependencies": {
6364
"@silverhand/eslint-config": "^6.0.1",
6465
"@silverhand/ts-config": "^6.0.0",
6566
"@silverhand/ts-config-react": "^6.0.0",
67+
"@types/cookie": "^0.6.0",
6668
"@vitest/coverage-v8": "^1.6.0",
6769
"eslint": "^8.57.0",
6870
"lint-staged": "^15.0.0",

packages/next/server-actions/client.test.ts

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { type SessionData } from '@logto/node';
2-
31
import { type LogtoNextConfig } from '../src/types.js';
42

53
import LogtoClient from './client.js';
@@ -15,8 +13,6 @@ const configs: LogtoNextConfig = {
1513
cookieSecure: false,
1614
};
1715

18-
const setItem = vi.fn();
19-
const getItem = vi.fn();
2016
const signIn = vi.fn();
2117
const handleSignInCallback = vi.fn();
2218
const getIdTokenClaims = vi.fn(() => ({
@@ -25,27 +21,12 @@ const getIdTokenClaims = vi.fn(() => ({
2521
const signOut = vi.fn();
2622
const getContext = vi.fn(async () => ({ isAuthenticated: true }));
2723

28-
vi.mock('../src/storage', () => ({
29-
default: vi.fn(() => ({
30-
setItem,
31-
getItem,
32-
removeItem: vi.fn(),
33-
destroy: vi.fn(),
34-
save: vi.fn(),
35-
})),
36-
}));
37-
3824
vi.mock('@logto/node', () => ({
39-
createSession: vi.fn((_, cookie: string) => {
40-
const data = JSON.parse(cookie) as SessionData;
41-
42-
const session = {
43-
...data,
44-
save: vi.fn(),
45-
getValues: vi.fn(async () => JSON.stringify(data)),
25+
CookieStorage: vi.fn((_, cookie: string) => {
26+
return {
27+
init: vi.fn(),
28+
destroy: vi.fn(),
4629
};
47-
48-
return session;
4930
}),
5031
}));
5132

@@ -76,18 +57,17 @@ describe('Next (server actions)', () => {
7657
});
7758

7859
describe('handleSignIn', () => {
79-
it('should get redirect url and new cookie', async () => {
60+
it('should get redirect url', async () => {
8061
const client = new LogtoClient(configs);
81-
const { url, newCookie } = await client.handleSignIn('{}', signInUrl);
62+
const { url } = await client.handleSignIn(signInUrl);
8263
expect(url).toEqual(signInUrl);
83-
expect(newCookie).not.toBeUndefined();
8464
});
8565
});
8666

8767
describe('handleSignInCallback', () => {
8868
it('should call nodClient.handleSignInCallback', async () => {
8969
const client = new LogtoClient(configs);
90-
await client.handleSignInCallback('{}', callbackUrl);
70+
await client.handleSignInCallback(callbackUrl);
9171
expect(handleSignInCallback).toHaveBeenCalledWith(callbackUrl);
9272
});
9373
});
@@ -103,15 +83,15 @@ describe('Next (server actions)', () => {
10383
describe('getLogtoContext', () => {
10484
it('should get context', async () => {
10585
const client = new LogtoClient(configs);
106-
const context = await client.getLogtoContext('{}');
86+
const context = await client.getLogtoContext();
10787
expect(context).toHaveProperty('isAuthenticated', true);
10888
});
10989
});
11090

111-
describe('createNodeClientFromHeaders', () => {
91+
describe('createNodeClient', () => {
11292
it('should get node client', async () => {
11393
const client = new LogtoClient(configs);
114-
const nodeClient = await client.createNodeClientFromHeaders('{}');
94+
const nodeClient = await client.createNodeClient();
11595
expect(nodeClient).toBeDefined();
11696
});
11797
});
Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use server';
22

3-
import { createSession, type GetContextParameters, type InteractionMode } from '@logto/node';
3+
import { CookieStorage, type GetContextParameters, type InteractionMode } from '@logto/node';
44
import NodeClient from '@logto/node/edge';
55

66
import BaseClient from '../src/client';
@@ -18,17 +18,15 @@ export default class LogtoClient extends BaseClient {
1818
/**
1919
* Init sign-in and return the url to redirect to Logto.
2020
*
21-
* @param cookie the raw cookie string
2221
* @param redirectUri the uri (callbackUri) to redirect to after sign in
2322
* @param interactionMode OIDC interaction mode
24-
* @returns the url to redirect to and new cookie if any
23+
* @returns the url to redirect
2524
*/
2625
async handleSignIn(
27-
cookie: string,
2826
redirectUri: string,
2927
interactionMode?: InteractionMode
3028
): Promise<{ url: string; newCookie?: string }> {
31-
const { nodeClient, session } = await this.createNodeClientFromHeaders(cookie);
29+
const nodeClient = await this.createNodeClient();
3230
await nodeClient.signIn(redirectUri, interactionMode);
3331

3432
if (!this.navigateUrl) {
@@ -38,20 +36,19 @@ export default class LogtoClient extends BaseClient {
3836

3937
return {
4038
url: this.navigateUrl,
41-
newCookie: await session.getValues?.(),
4239
};
4340
}
4441

4542
/**
4643
* Init sign-out and return the url to redirect to Logto.
4744
*
48-
* @param cookie the raw cookie string
4945
* @param redirectUri the uri (postSignOutUri) to redirect to after sign out
5046
* @returns the url to redirect to
5147
*/
52-
async handleSignOut(cookie: string, redirectUri = this.config.baseUrl): Promise<string> {
53-
const { nodeClient } = await this.createNodeClientFromHeaders(cookie);
48+
async handleSignOut(redirectUri = this.config.baseUrl): Promise<string> {
49+
const nodeClient = await this.createNodeClient();
5450
await nodeClient.signOut(redirectUri);
51+
await this.storage?.destroy();
5552

5653
if (!this.navigateUrl) {
5754
// Not expected to happen
@@ -64,42 +61,51 @@ export default class LogtoClient extends BaseClient {
6461
/**
6562
* Handle sign-in callback from Logto.
6663
*
67-
* @param cookie the raw cookie string
6864
* @param callbackUrl the uri (callbackUri) to redirect to after sign in, should match the one used in handleSignIn
69-
* @returns new cookie if any
7065
*/
71-
async handleSignInCallback(cookie: string, callbackUrl: string): Promise<string | undefined> {
72-
const { nodeClient, session } = await this.createNodeClientFromHeaders(cookie);
66+
async handleSignInCallback(callbackUrl: string) {
67+
const nodeClient = await this.createNodeClient();
7368

7469
await nodeClient.handleSignInCallback(callbackUrl);
75-
return session.getValues?.();
7670
}
7771

7872
/**
7973
* Get Logto context from cookies.
8074
*
81-
* @param cookie the raw cookie string
8275
* @param config additional configs of GetContextParameters
8376
* @returns LogtoContext
8477
*/
85-
async getLogtoContext(cookie: string, config: GetContextParameters = {}) {
86-
const { nodeClient } = await this.createNodeClientFromHeaders(cookie);
78+
async getLogtoContext(config: GetContextParameters = {}) {
79+
const nodeClient = await this.createNodeClient({ ignoreCookieChange: true });
8780
const context = await nodeClient.getContext(config);
8881

8982
return context;
9083
}
9184

92-
async createNodeClientFromHeaders(cookie: string) {
93-
const session = await createSession(
94-
{
95-
secret: this.config.cookieSecret,
96-
crypto,
85+
async createNodeClient({ ignoreCookieChange }: { ignoreCookieChange?: boolean } = {}) {
86+
const { cookies } = await import('next/headers');
87+
this.storage = new CookieStorage({
88+
encryptionKey: this.config.cookieSecret,
89+
cookieKey: `logto:${this.config.appId}`,
90+
isSecure: this.config.cookieSecure,
91+
getCookie: (...args) => {
92+
return cookies().get(...args)?.value ?? '';
9793
},
98-
cookie
99-
);
94+
setCookie: (...args) => {
95+
// In server component (RSC), it is not allowed to modify cookies, see https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options.
96+
if (!ignoreCookieChange) {
97+
cookies().set(...args);
98+
}
99+
},
100+
});
100101

101-
const nodeClient = super.createNodeClient(session);
102+
await this.storage.init();
102103

103-
return { nodeClient, session };
104+
return new this.adapters.NodeClient(this.config, {
105+
storage: this.storage,
106+
navigate: (url) => {
107+
this.navigateUrl = url;
108+
},
109+
});
104110
}
105111
}

packages/next/server-actions/cookie.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)