Skip to content

Commit 6b34937

Browse files
authored
feat: adds and exports reusable auth server functions (#11900)
### What Adds exportable server functions for `login`, `logout` and `refresh` that are fully typed and ready to use. ### Why Creating server functions for these auth operations require the developer to manually set and handle the cookies / auth JWT. This can be a complex and involved process - instead we want to provide an option that will handle the cookies internally and simplify the process for the user. ### How Three re-usable functions can be exported from `@payload/next/server-functions`: - login - logout - refresh Examples of how to use these functions will be added to the docs shortly, along with more in-depth info on server functions.
1 parent 39462bc commit 6b34937

21 files changed

+922
-5
lines changed

docs/authentication/operations.mdx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,21 @@ mutation {
158158

159159
```ts
160160
const result = await payload.login({
161-
collection: '[collection-slug]',
161+
collection: 'collection-slug',
162162
data: {
163163
email: 'dev@payloadcms.com',
164164
password: 'get-out',
165165
},
166166
})
167167
```
168168

169+
<Banner type="success">
170+
**Server Functions:** Payload offers a ready-to-use `login` server function
171+
that utilizes the Local API. For integration details and examples, check out
172+
the [Server Function
173+
docs](../local-api/server-functions#reusable-payload-server-functions).
174+
</Banner>
175+
169176
## Logout
170177

171178
As Payload sets HTTP-only cookies, logging out cannot be done by just removing a cookie in JavaScript, as HTTP-only cookies are inaccessible by JS within the browser. So, Payload exposes a `logout` operation to delete the token in a safe way.
@@ -189,6 +196,13 @@ mutation {
189196
}
190197
```
191198

199+
<Banner type="success">
200+
**Server Functions:** Payload provides a ready-to-use `logout` server function
201+
that manages the user's cookie for a seamless logout. For integration details
202+
and examples, check out the [Server Function
203+
docs](../local-api/server-functions#reusable-payload-server-functions).
204+
</Banner>
205+
192206
## Refresh
193207

194208
Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user.
@@ -240,6 +254,13 @@ mutation {
240254
}
241255
```
242256

257+
<Banner type="success">
258+
**Server Functions:** Payload exports a ready-to-use `refresh` server function
259+
that automatically renews the user's token and updates the associated cookie.
260+
For integration details and examples, check out the [Server Function
261+
docs](../local-api/server-functions#reusable-payload-server-functions).
262+
</Banner>
263+
243264
## Verify by Email
244265

245266
If your collection supports email verification, the Verify operation will be exposed which accepts a verification token and sets the user's `_verified` property to `true`, thereby allowing the user to authenticate with the Payload API.
@@ -270,7 +291,7 @@ mutation {
270291

271292
```ts
272293
const result = await payload.verifyEmail({
273-
collection: '[collection-slug]',
294+
collection: 'collection-slug',
274295
token: 'TOKEN_HERE',
275296
})
276297
```
@@ -308,7 +329,7 @@ mutation {
308329

309330
```ts
310331
const result = await payload.unlock({
311-
collection: '[collection-slug]',
332+
collection: 'collection-slug',
312333
})
313334
```
314335

@@ -349,7 +370,7 @@ mutation {
349370

350371
```ts
351372
const token = await payload.forgotPassword({
352-
collection: '[collection-slug]',
373+
collection: 'collection-slug',
353374
data: {
354375
email: 'dev@payloadcms.com',
355376
},

docs/local-api/server-functions.mdx

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,168 @@ export const PostForm: React.FC = () => {
310310

311311
## Reusable Payload Server Functions
312312

313-
Coming soon…
313+
Managing authentication with the Local API can be tricky as you have to handle cookies and tokens yourself, and there aren't built-in logout or refresh functions since these only modify cookies. To make this easier, we provide `login`, `logout`, and `refresh` as ready-to-use server functions. They take care of the underlying complexity so you don't have to.
314+
315+
### `login`
316+
317+
Logs in a user by verifying credentials and setting the authentication cookie. This function allows login via username or email, depending on the collection auth configuration.
318+
319+
#### Importing the `login` function
320+
321+
```ts
322+
import { login } from '@payloadcms/next/auth'
323+
```
324+
325+
The login function needs your Payload config, which cannot be imported in a client component. To work around this, create a simple server function like the one below, and call it from your client.
326+
327+
```ts
328+
'use server'
329+
330+
import { login } from '@payloadcms/next/auth'
331+
import config from '@payload-config'
332+
333+
export async function loginAction({
334+
email,
335+
password,
336+
}: {
337+
email: string
338+
password: string
339+
}) {
340+
try {
341+
const result = await login({
342+
collection: 'users',
343+
config,
344+
email,
345+
password,
346+
})
347+
return result
348+
} catch (error) {
349+
throw new Error(
350+
`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
351+
)
352+
}
353+
}
354+
```
355+
356+
#### Login from the React Client Component
357+
358+
```tsx
359+
'use client'
360+
import { useState } from 'react'
361+
import { loginAction } from '../loginAction'
362+
363+
export default function LoginForm() {
364+
const [email, setEmail] = useState<string>('')
365+
const [password, setPassword] = useState<string>('')
366+
367+
return (
368+
<form onSubmit={() => loginAction({ email, password })}>
369+
<label htmlFor="email">Email</label>
370+
<input
371+
id="email"
372+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
373+
setEmail(e.target.value)
374+
}
375+
type="email"
376+
value={email}
377+
/>
378+
<label htmlFor="password">Password</label>
379+
<input
380+
id="password"
381+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
382+
setPassword(e.target.value)
383+
}
384+
type="password"
385+
value={password}
386+
/>
387+
<button type="submit">Login</button>
388+
</form>
389+
)
390+
}
391+
```
392+
393+
### `logout`
394+
395+
Logs out the current user by clearing the authentication cookie.
396+
397+
#### Importing the `logout` function
398+
399+
```ts
400+
import { logout } from '@payloadcms/next/auth'
401+
```
402+
403+
Similar to the login function, you now need to pass your Payload config to this function and this cannot be done in a client component. Use a helper server function as shown below.
404+
405+
```ts
406+
'use server'
407+
408+
import { logout } from '@payloadcms/next/auth'
409+
import config from '@payload-config'
410+
411+
export async function logoutAction() {
412+
try {
413+
return await logout({ config })
414+
} catch (error) {
415+
throw new Error(
416+
`Logout failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
417+
)
418+
}
419+
}
420+
```
421+
422+
#### Logout from the React Client Component
423+
424+
```tsx
425+
'use client'
426+
import { logoutAction } from '../logoutAction'
427+
428+
export default function LogoutButton() {
429+
return <button onClick={() => logoutFunction()}>Logout</button>
430+
}
431+
```
432+
433+
### `refresh`
434+
435+
Refreshes the authentication token for the logged-in user.
436+
437+
#### Importing the `refresh` function
438+
439+
```ts
440+
import { refresh } from '@payloadcms/next/auth'
441+
```
442+
443+
As with login and logout, you need to pass your Payload config to this function. Create a helper server function like the one below. Passing the config directly to the client is not possible and will throw errors.
444+
445+
```ts
446+
'use server'
447+
448+
import { refresh } from '@payloadcms/next/auth'
449+
import config from '@payload-config'
450+
451+
export async function refreshAction() {
452+
try {
453+
return await refresh({
454+
collection: 'users', // update this to your collection slug
455+
config,
456+
})
457+
} catch (error) {
458+
throw new Error(
459+
`Refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
460+
)
461+
}
462+
}
463+
```
464+
465+
#### Using Refresh from the React Client Component
466+
467+
```tsx
468+
'use client'
469+
import { refreshAction } from '../actions/refreshAction'
470+
471+
export default function RefreshTokenButton() {
472+
return <button onClick={() => refreshFunction()}>Refresh</button>
473+
}
474+
```
314475

315476
## Error Handling in Server Functions
316477

packages/next/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
"types": "./src/exports/routes.ts",
3838
"default": "./src/exports/routes.ts"
3939
},
40+
"./auth": {
41+
"import": "./src/exports/auth.ts",
42+
"types": "./src/exports/auth.ts",
43+
"default": "./src/exports/auth.ts"
44+
},
4045
"./templates": {
4146
"import": "./src/exports/templates.ts",
4247
"types": "./src/exports/templates.ts",

packages/next/src/auth/login.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use server'
2+
3+
import type { CollectionSlug } from 'payload'
4+
5+
import { cookies as getCookies } from 'next/headers.js'
6+
import { generatePayloadCookie, getPayload } from 'payload'
7+
8+
import { setPayloadAuthCookie } from '../utilities/setPayloadAuthCookie.js'
9+
10+
type LoginWithEmail = {
11+
collection: CollectionSlug
12+
config: any
13+
email: string
14+
password: string
15+
username?: never
16+
}
17+
18+
type LoginWithUsername = {
19+
collection: CollectionSlug
20+
config: any
21+
email?: never
22+
password: string
23+
username: string
24+
}
25+
type LoginArgs = LoginWithEmail | LoginWithUsername
26+
27+
export async function login({ collection, config, email, password, username }: LoginArgs): Promise<{
28+
token?: string
29+
user: any
30+
}> {
31+
const payload = await getPayload({ config })
32+
33+
const authConfig = payload.collections[collection]?.config.auth
34+
if (!authConfig) {
35+
throw new Error(`No auth config found for collection: ${collection}`)
36+
}
37+
38+
const loginWithUsername = authConfig?.loginWithUsername ?? false
39+
40+
if (loginWithUsername) {
41+
if (loginWithUsername.allowEmailLogin) {
42+
if (!email && !username) {
43+
throw new Error('Email or username is required.')
44+
}
45+
} else {
46+
if (!username) {
47+
throw new Error('Username is required.')
48+
}
49+
}
50+
} else {
51+
if (!email) {
52+
throw new Error('Email is required.')
53+
}
54+
}
55+
56+
let loginData
57+
58+
if (loginWithUsername) {
59+
loginData = username ? { password, username } : { email, password }
60+
} else {
61+
loginData = { email, password }
62+
}
63+
64+
try {
65+
const result = await payload.login({
66+
collection,
67+
data: loginData,
68+
})
69+
70+
if (result.token) {
71+
await setPayloadAuthCookie({
72+
authConfig,
73+
cookiePrefix: payload.config.cookiePrefix,
74+
token: result.token,
75+
})
76+
}
77+
78+
if ('removeTokenFromResponses' in config && config.removeTokenFromResponses) {
79+
delete result.token
80+
}
81+
82+
return result
83+
} catch (e) {
84+
console.error('Login error:', e)
85+
throw new Error(`${e}`)
86+
}
87+
}

packages/next/src/auth/logout.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use server'
2+
3+
import { cookies as getCookies, headers as nextHeaders } from 'next/headers.js'
4+
import { getPayload } from 'payload'
5+
6+
import { getExistingAuthToken } from '../utilities/getExistingAuthToken.js'
7+
8+
export async function logout({ config }: { config: any }) {
9+
try {
10+
const payload = await getPayload({ config })
11+
const headers = await nextHeaders()
12+
const result = await payload.auth({ headers })
13+
14+
if (!result.user) {
15+
return { message: 'User already logged out', success: true }
16+
}
17+
18+
const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)
19+
20+
if (existingCookie) {
21+
const cookies = await getCookies()
22+
cookies.delete(existingCookie.name)
23+
return { message: 'User logged out successfully', success: true }
24+
}
25+
} catch (e) {
26+
console.error('Logout error:', e)
27+
throw new Error(`${e}`)
28+
}
29+
}

0 commit comments

Comments
 (0)