Skip to content

Commit 146eebe

Browse files
committed
feat: Add getPayloadSession & usePayloadSession
1 parent becc12a commit 146eebe

File tree

10 files changed

+213
-3
lines changed

10 files changed

+213
-3
lines changed

packages/dev/.env.example

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
NEXT_PUBLIC_SERVER_URL=http://localhost:5000
2-
31
# Payload CMS
42
PAYLOAD_SECRET=secret
53
DATABASE_URI=postgres://root:root@127.0.0.1:5432/payload-authjs

packages/dev/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"payload/generated-types": ["./src/payload-types.ts"],
2525
"@/*": ["./src/*"],
2626
"payload-authjs": ["../payload-authjs/src/index.ts"],
27-
"payload-authjs/components": ["../payload-authjs/src/components/index.ts"]
27+
"payload-authjs/components": ["../payload-authjs/src/components/index.ts"],
28+
"payload-authjs/client": ["../payload-authjs/src/client.ts"]
2829
}
2930
},
3031
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],

packages/payload-authjs/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
"import": "./dist/index.js",
1616
"default": "./dist/index.js"
1717
},
18+
"./client": {
19+
"types": "./dist/client.d.ts",
20+
"import": "./dist/client.js",
21+
"default": "./dist/client.js"
22+
},
1823
"./middleware": {
1924
"types": "./dist/payload/middleware.d.ts",
2025
"import": "./dist/payload/middleware.js",

packages/payload-authjs/src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { usePayloadSession } from "./payload/session/usePayloadSession";

packages/payload-authjs/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ export { type PayloadAuthjsUser } from "./authjs/types";
33
export { withPayload } from "./authjs/withPayload";
44
export { getPayloadUser } from "./payload/getPayloadUser";
55
export { authjsPlugin, type AuthjsPluginConfig } from "./payload/plugin";
6+
export { getPayloadSession, type PayloadSession } from "./payload/session/getPayloadSession";
7+
export { PayloadSessionProviderWrapper as PayloadSessionProvider } from "./payload/session/PayloadSessionProviderWrapper";

packages/payload-authjs/src/payload/getPayloadUser.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ interface Options<TSlug extends CollectionSlug> {
1818

1919
/**
2020
* Get the payload user from the server (only works on the server side)
21+
*
22+
* @deprecated Use `getPayloadSession` instead
2123
*/
2224
export const getPayloadUser = async <TSlug extends CollectionSlug = "users">({
2325
serverUrl = process.env.NEXT_PUBLIC_SERVER_URL,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"use client";
2+
3+
import type { CollectionSlug, DataFromCollectionSlug } from "payload";
4+
import { createContext, type ReactNode, useState } from "react";
5+
import type { PayloadSession } from "./getPayloadSession";
6+
7+
export interface SessionContext<TSlug extends CollectionSlug> {
8+
/**
9+
* The session
10+
*/
11+
session: PayloadSession<TSlug> | null;
12+
/**
13+
* Function to refresh the session
14+
*/
15+
refresh: () => Promise<PayloadSession<TSlug> | null>;
16+
}
17+
18+
export const Context = createContext<SessionContext<never>>({
19+
session: null,
20+
refresh: () => new Promise(resolve => resolve(null)),
21+
});
22+
23+
interface Props<TSlug extends CollectionSlug> {
24+
/**
25+
* The slug of the collection that contains the users
26+
*
27+
* @default "users"
28+
*/
29+
userCollectionSlug?: TSlug;
30+
/**
31+
* The session (if available)
32+
*/
33+
session: PayloadSession<TSlug> | null;
34+
/**
35+
* The children to render
36+
*/
37+
children: ReactNode;
38+
}
39+
40+
/**
41+
* PayloadSessionProvider (client-side) that provides the session to the context provider
42+
*/
43+
export const PayloadSessionProvider = <TSlug extends CollectionSlug = "users">({
44+
userCollectionSlug = "users" as TSlug,
45+
session,
46+
children,
47+
}: Props<TSlug>) => {
48+
const [localSession, setLocalSession] = useState<PayloadSession<TSlug> | null>(session);
49+
50+
/**
51+
* Function to refresh the session
52+
*/
53+
const refresh = async () => {
54+
// Refresh the session on the server
55+
const response = await fetch(`/api/${userCollectionSlug}/refresh-token`, {
56+
method: "POST",
57+
});
58+
const result: { user: DataFromCollectionSlug<TSlug>; exp: number } = await response.json();
59+
60+
// If the response is not ok or the user is not present, return null
61+
if (!response.ok || !result.user) {
62+
return null;
63+
}
64+
65+
// Update the local session
66+
const localSession = {
67+
user: result.user,
68+
expires: new Date(result.exp * 1000).toISOString(),
69+
};
70+
setLocalSession(localSession);
71+
72+
// Return the session
73+
return localSession;
74+
};
75+
76+
return (
77+
<Context
78+
value={{
79+
session: localSession,
80+
refresh,
81+
}}
82+
>
83+
{children}
84+
</Context>
85+
);
86+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { CollectionSlug } from "payload";
2+
import { type ReactNode } from "react";
3+
import { getPayloadSession } from "./getPayloadSession";
4+
import { PayloadSessionProvider } from "./PayloadSessionProvider";
5+
6+
interface Props<TSlug extends CollectionSlug> {
7+
/**
8+
* The slug of the collection that contains the users
9+
*
10+
* @default "users"
11+
*/
12+
userCollectionSlug?: TSlug;
13+
/**
14+
* The children to render
15+
*/
16+
children: ReactNode;
17+
}
18+
19+
/**
20+
* PayloadSessionProvider (server-side wrapper) that fetches the session and provides it to the context provider
21+
*/
22+
export const PayloadSessionProviderWrapper = async <TSlug extends CollectionSlug = "users">({
23+
userCollectionSlug,
24+
children,
25+
}: Props<TSlug>) => {
26+
const session = await getPayloadSession({ userCollectionSlug });
27+
28+
return (
29+
<PayloadSessionProvider userCollectionSlug={userCollectionSlug} session={session}>
30+
{children}
31+
</PayloadSessionProvider>
32+
);
33+
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { cookies, headers } from "next/headers";
2+
import type { CollectionSlug, DataFromCollectionSlug } from "payload";
3+
4+
interface Options<TSlug extends CollectionSlug> {
5+
/**
6+
* The slug of the collection that contains the users
7+
*
8+
* @default "users"
9+
*/
10+
userCollectionSlug?: TSlug;
11+
}
12+
13+
export interface PayloadSession<TSlug extends CollectionSlug> {
14+
user: DataFromCollectionSlug<TSlug>;
15+
expires: string;
16+
}
17+
18+
/**
19+
* Get the payload session from the server side
20+
*/
21+
export const getPayloadSession = async <TSlug extends CollectionSlug = "users">({
22+
userCollectionSlug = "users" as TSlug,
23+
}: Options<TSlug> = {}): Promise<PayloadSession<TSlug> | null> => {
24+
// Get the server URL
25+
const serverUrl = await getServerUrl();
26+
27+
// Fetch the session from the server
28+
const response = await fetch(`${serverUrl}/api/${userCollectionSlug}/me`, {
29+
headers: {
30+
Cookie: (await cookies()).toString(),
31+
},
32+
});
33+
const result: { user: DataFromCollectionSlug<TSlug>; exp: number } = await response.json();
34+
35+
// If the response is not ok or the user is not present, return null
36+
if (!response.ok || !result.user) {
37+
return null;
38+
}
39+
40+
// Return the session
41+
return {
42+
user: result.user,
43+
expires: new Date(result.exp * 1000).toISOString(),
44+
};
45+
};
46+
47+
/**
48+
* Get the server URL from the environment variables or the request headers
49+
*/
50+
const getServerUrl = async () => {
51+
let serverUrl = process.env.NEXT_PUBLIC_SERVER_URL;
52+
53+
if (!serverUrl) {
54+
const requestHeaders = await headers();
55+
56+
const detectedHost = requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host");
57+
const detectedProtocol = requestHeaders.get("x-forwarded-proto") ?? "https";
58+
const protocol = detectedProtocol.endsWith(":") ? detectedProtocol : detectedProtocol + ":";
59+
60+
serverUrl = `${protocol}//${detectedHost}`;
61+
}
62+
63+
return serverUrl;
64+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use client";
2+
3+
import type { CollectionSlug } from "payload";
4+
import { use } from "react";
5+
import { Context, type SessionContext } from "./PayloadSessionProvider";
6+
7+
/**
8+
* Client side hook to retrieve the session from the context provider (PayloadSessionProvider)
9+
*/
10+
export const usePayloadSession = <TSlug extends CollectionSlug = "users">() => {
11+
const result = use<SessionContext<TSlug>>(Context);
12+
13+
if (!result) {
14+
throw new Error("usePayloadSession must be used within a PayloadSessionProvider");
15+
}
16+
17+
return result;
18+
};

0 commit comments

Comments
 (0)