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.
This commit is contained in:
committed by
GitHub
parent
39462bc6b9
commit
6b349378e0
@@ -158,7 +158,7 @@ mutation {
|
||||
|
||||
```ts
|
||||
const result = await payload.login({
|
||||
collection: '[collection-slug]',
|
||||
collection: 'collection-slug',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'get-out',
|
||||
@@ -166,6 +166,13 @@ const result = await payload.login({
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Server Functions:** Payload offers a ready-to-use `login` server function
|
||||
that utilizes the Local API. For integration details and examples, check out
|
||||
the [Server Function
|
||||
docs](../local-api/server-functions#reusable-payload-server-functions).
|
||||
</Banner>
|
||||
|
||||
## Logout
|
||||
|
||||
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 {
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Server Functions:** Payload provides a ready-to-use `logout` server function
|
||||
that manages the user's cookie for a seamless logout. For integration details
|
||||
and examples, check out the [Server Function
|
||||
docs](../local-api/server-functions#reusable-payload-server-functions).
|
||||
</Banner>
|
||||
|
||||
## Refresh
|
||||
|
||||
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 {
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
**Server Functions:** Payload exports a ready-to-use `refresh` server function
|
||||
that automatically renews the user's token and updates the associated cookie.
|
||||
For integration details and examples, check out the [Server Function
|
||||
docs](../local-api/server-functions#reusable-payload-server-functions).
|
||||
</Banner>
|
||||
|
||||
## Verify by Email
|
||||
|
||||
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 {
|
||||
|
||||
```ts
|
||||
const result = await payload.verifyEmail({
|
||||
collection: '[collection-slug]',
|
||||
collection: 'collection-slug',
|
||||
token: 'TOKEN_HERE',
|
||||
})
|
||||
```
|
||||
@@ -308,7 +329,7 @@ mutation {
|
||||
|
||||
```ts
|
||||
const result = await payload.unlock({
|
||||
collection: '[collection-slug]',
|
||||
collection: 'collection-slug',
|
||||
})
|
||||
```
|
||||
|
||||
@@ -349,7 +370,7 @@ mutation {
|
||||
|
||||
```ts
|
||||
const token = await payload.forgotPassword({
|
||||
collection: '[collection-slug]',
|
||||
collection: 'collection-slug',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
},
|
||||
|
||||
@@ -310,7 +310,168 @@ export const PostForm: React.FC = () => {
|
||||
|
||||
## Reusable Payload Server Functions
|
||||
|
||||
Coming soon…
|
||||
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.
|
||||
|
||||
### `login`
|
||||
|
||||
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.
|
||||
|
||||
#### Importing the `login` function
|
||||
|
||||
```ts
|
||||
import { login } from '@payloadcms/next/auth'
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
|
||||
import { login } from '@payloadcms/next/auth'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function loginAction({
|
||||
email,
|
||||
password,
|
||||
}: {
|
||||
email: string
|
||||
password: string
|
||||
}) {
|
||||
try {
|
||||
const result = await login({
|
||||
collection: 'users',
|
||||
config,
|
||||
email,
|
||||
password,
|
||||
})
|
||||
return result
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Login from the React Client Component
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { loginAction } from '../loginAction'
|
||||
|
||||
export default function LoginForm() {
|
||||
const [email, setEmail] = useState<string>('')
|
||||
const [password, setPassword] = useState<string>('')
|
||||
|
||||
return (
|
||||
<form onSubmit={() => loginAction({ email, password })}>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setEmail(e.target.value)
|
||||
}
|
||||
type="email"
|
||||
value={email}
|
||||
/>
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setPassword(e.target.value)
|
||||
}
|
||||
type="password"
|
||||
value={password}
|
||||
/>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### `logout`
|
||||
|
||||
Logs out the current user by clearing the authentication cookie.
|
||||
|
||||
#### Importing the `logout` function
|
||||
|
||||
```ts
|
||||
import { logout } from '@payloadcms/next/auth'
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
|
||||
import { logout } from '@payloadcms/next/auth'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function logoutAction() {
|
||||
try {
|
||||
return await logout({ config })
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Logout failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Logout from the React Client Component
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { logoutAction } from '../logoutAction'
|
||||
|
||||
export default function LogoutButton() {
|
||||
return <button onClick={() => logoutFunction()}>Logout</button>
|
||||
}
|
||||
```
|
||||
|
||||
### `refresh`
|
||||
|
||||
Refreshes the authentication token for the logged-in user.
|
||||
|
||||
#### Importing the `refresh` function
|
||||
|
||||
```ts
|
||||
import { refresh } from '@payloadcms/next/auth'
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
|
||||
import { refresh } from '@payloadcms/next/auth'
|
||||
import config from '@payload-config'
|
||||
|
||||
export async function refreshAction() {
|
||||
try {
|
||||
return await refresh({
|
||||
collection: 'users', // update this to your collection slug
|
||||
config,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Refresh from the React Client Component
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { refreshAction } from '../actions/refreshAction'
|
||||
|
||||
export default function RefreshTokenButton() {
|
||||
return <button onClick={() => refreshFunction()}>Refresh</button>
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling in Server Functions
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
"types": "./src/exports/routes.ts",
|
||||
"default": "./src/exports/routes.ts"
|
||||
},
|
||||
"./auth": {
|
||||
"import": "./src/exports/auth.ts",
|
||||
"types": "./src/exports/auth.ts",
|
||||
"default": "./src/exports/auth.ts"
|
||||
},
|
||||
"./templates": {
|
||||
"import": "./src/exports/templates.ts",
|
||||
"types": "./src/exports/templates.ts",
|
||||
|
||||
87
packages/next/src/auth/login.ts
Normal file
87
packages/next/src/auth/login.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
'use server'
|
||||
|
||||
import type { CollectionSlug } from 'payload'
|
||||
|
||||
import { cookies as getCookies } from 'next/headers.js'
|
||||
import { generatePayloadCookie, getPayload } from 'payload'
|
||||
|
||||
import { setPayloadAuthCookie } from '../utilities/setPayloadAuthCookie.js'
|
||||
|
||||
type LoginWithEmail = {
|
||||
collection: CollectionSlug
|
||||
config: any
|
||||
email: string
|
||||
password: string
|
||||
username?: never
|
||||
}
|
||||
|
||||
type LoginWithUsername = {
|
||||
collection: CollectionSlug
|
||||
config: any
|
||||
email?: never
|
||||
password: string
|
||||
username: string
|
||||
}
|
||||
type LoginArgs = LoginWithEmail | LoginWithUsername
|
||||
|
||||
export async function login({ collection, config, email, password, username }: LoginArgs): Promise<{
|
||||
token?: string
|
||||
user: any
|
||||
}> {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
const authConfig = payload.collections[collection]?.config.auth
|
||||
if (!authConfig) {
|
||||
throw new Error(`No auth config found for collection: ${collection}`)
|
||||
}
|
||||
|
||||
const loginWithUsername = authConfig?.loginWithUsername ?? false
|
||||
|
||||
if (loginWithUsername) {
|
||||
if (loginWithUsername.allowEmailLogin) {
|
||||
if (!email && !username) {
|
||||
throw new Error('Email or username is required.')
|
||||
}
|
||||
} else {
|
||||
if (!username) {
|
||||
throw new Error('Username is required.')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!email) {
|
||||
throw new Error('Email is required.')
|
||||
}
|
||||
}
|
||||
|
||||
let loginData
|
||||
|
||||
if (loginWithUsername) {
|
||||
loginData = username ? { password, username } : { email, password }
|
||||
} else {
|
||||
loginData = { email, password }
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await payload.login({
|
||||
collection,
|
||||
data: loginData,
|
||||
})
|
||||
|
||||
if (result.token) {
|
||||
await setPayloadAuthCookie({
|
||||
authConfig,
|
||||
cookiePrefix: payload.config.cookiePrefix,
|
||||
token: result.token,
|
||||
})
|
||||
}
|
||||
|
||||
if ('removeTokenFromResponses' in config && config.removeTokenFromResponses) {
|
||||
delete result.token
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error('Login error:', e)
|
||||
throw new Error(`${e}`)
|
||||
}
|
||||
}
|
||||
29
packages/next/src/auth/logout.ts
Normal file
29
packages/next/src/auth/logout.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
'use server'
|
||||
|
||||
import { cookies as getCookies, headers as nextHeaders } from 'next/headers.js'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import { getExistingAuthToken } from '../utilities/getExistingAuthToken.js'
|
||||
|
||||
export async function logout({ config }: { config: any }) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
const headers = await nextHeaders()
|
||||
const result = await payload.auth({ headers })
|
||||
|
||||
if (!result.user) {
|
||||
return { message: 'User already logged out', success: true }
|
||||
}
|
||||
|
||||
const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)
|
||||
|
||||
if (existingCookie) {
|
||||
const cookies = await getCookies()
|
||||
cookies.delete(existingCookie.name)
|
||||
return { message: 'User logged out successfully', success: true }
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Logout error:', e)
|
||||
throw new Error(`${e}`)
|
||||
}
|
||||
}
|
||||
42
packages/next/src/auth/refresh.ts
Normal file
42
packages/next/src/auth/refresh.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
'use server'
|
||||
|
||||
import type { CollectionSlug } from 'payload'
|
||||
|
||||
import { headers as nextHeaders } from 'next/headers.js'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import { getExistingAuthToken } from '../utilities/getExistingAuthToken.js'
|
||||
import { setPayloadAuthCookie } from '../utilities/setPayloadAuthCookie.js'
|
||||
|
||||
export async function refresh({ collection, config }: { collection: CollectionSlug; config: any }) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
const authConfig = payload.collections[collection]?.config.auth
|
||||
|
||||
if (!authConfig) {
|
||||
throw new Error(`No auth config found for collection: ${collection}`)
|
||||
}
|
||||
|
||||
const { user } = await payload.auth({ headers: await nextHeaders() })
|
||||
if (!user) {
|
||||
throw new Error('User not authenticated')
|
||||
}
|
||||
|
||||
const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)
|
||||
|
||||
if (!existingCookie) {
|
||||
return { message: 'No valid token found', success: false }
|
||||
}
|
||||
|
||||
await setPayloadAuthCookie({
|
||||
authConfig,
|
||||
cookiePrefix: payload.config.cookiePrefix,
|
||||
token: existingCookie.value,
|
||||
})
|
||||
|
||||
return { message: 'Token refreshed successfully', success: true }
|
||||
} catch (e) {
|
||||
console.error('Refresh error:', e)
|
||||
throw new Error(`${e}`)
|
||||
}
|
||||
}
|
||||
3
packages/next/src/exports/auth.ts
Normal file
3
packages/next/src/exports/auth.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { login } from '../auth/login.js'
|
||||
export { logout } from '../auth/logout.js'
|
||||
export { refresh } from '../auth/refresh.js'
|
||||
10
packages/next/src/utilities/getExistingAuthToken.ts
Normal file
10
packages/next/src/utilities/getExistingAuthToken.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { cookies as getCookies } from 'next/headers.js'
|
||||
|
||||
type Cookie = {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
export async function getExistingAuthToken(cookiePrefix: string): Promise<Cookie | undefined> {
|
||||
const cookies = await getCookies()
|
||||
return cookies.getAll().find((cookie) => cookie.name.startsWith(cookiePrefix))
|
||||
}
|
||||
42
packages/next/src/utilities/setPayloadAuthCookie.ts
Normal file
42
packages/next/src/utilities/setPayloadAuthCookie.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Auth } from 'payload'
|
||||
|
||||
import { cookies as getCookies } from 'next/headers.js'
|
||||
import { generatePayloadCookie } from 'payload'
|
||||
|
||||
type SetPayloadAuthCookieArgs = {
|
||||
authConfig: Auth
|
||||
cookiePrefix: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export async function setPayloadAuthCookie({
|
||||
authConfig,
|
||||
cookiePrefix,
|
||||
token,
|
||||
}: SetPayloadAuthCookieArgs): Promise<void> {
|
||||
const cookies = await getCookies()
|
||||
|
||||
const cookieExpiration = authConfig.tokenExpiration
|
||||
? new Date(Date.now() + authConfig.tokenExpiration)
|
||||
: undefined
|
||||
|
||||
const payloadCookie = generatePayloadCookie({
|
||||
collectionAuthConfig: authConfig,
|
||||
cookiePrefix,
|
||||
expires: cookieExpiration,
|
||||
returnCookieAsObject: true,
|
||||
token,
|
||||
})
|
||||
|
||||
if (payloadCookie.value) {
|
||||
cookies.set(payloadCookie.name, payloadCookie.value, {
|
||||
domain: authConfig.cookies.domain,
|
||||
expires: payloadCookie.expires ? new Date(payloadCookie.expires) : undefined,
|
||||
httpOnly: true,
|
||||
sameSite: (typeof authConfig.cookies.sameSite === 'string'
|
||||
? authConfig.cookies.sameSite.toLowerCase()
|
||||
: 'lax') as 'lax' | 'none' | 'strict',
|
||||
secure: authConfig.cookies.secure || false,
|
||||
})
|
||||
}
|
||||
}
|
||||
36
test/server-functions/components/login.tsx
Normal file
36
test/server-functions/components/login.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import { type ChangeEvent, type FormEvent, useState } from 'react'
|
||||
|
||||
import { loginFunction } from './loginFunction.js'
|
||||
|
||||
const LoginForm = () => {
|
||||
const [email, setEmail] = useState<string>('')
|
||||
const [password, setPassword] = useState<string>('')
|
||||
|
||||
return (
|
||||
<form onSubmit={() => loginFunction({ email, password })}>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
required
|
||||
type="email"
|
||||
value={email}
|
||||
/>
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
required
|
||||
type="password"
|
||||
value={password}
|
||||
/>
|
||||
<button type="submit">Custom Login</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginForm
|
||||
23
test/server-functions/components/loginFunction.tsx
Normal file
23
test/server-functions/components/loginFunction.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use server'
|
||||
import { login } from '@payloadcms/next/auth'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
type LoginArgs = {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export async function loginFunction({ email, password }: LoginArgs) {
|
||||
try {
|
||||
const result = await login({
|
||||
collection: 'users',
|
||||
config,
|
||||
email,
|
||||
password,
|
||||
})
|
||||
return result
|
||||
} catch (error) {
|
||||
throw new Error(`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
8
test/server-functions/components/logout.tsx
Normal file
8
test/server-functions/components/logout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { logoutFunction } from './logoutFunction.js'
|
||||
|
||||
const LogoutButton = () => {
|
||||
return <button onClick={() => logoutFunction()}>Custom Logout</button>
|
||||
}
|
||||
export default LogoutButton
|
||||
14
test/server-functions/components/logoutFunction.tsx
Normal file
14
test/server-functions/components/logoutFunction.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
'use server'
|
||||
import { logout } from '@payloadcms/next/auth'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
export async function logoutFunction() {
|
||||
try {
|
||||
return await logout({
|
||||
config,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Logout failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
7
test/server-functions/components/refresh.tsx
Normal file
7
test/server-functions/components/refresh.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client'
|
||||
import { refreshFunction } from './refreshFunction.js'
|
||||
|
||||
const RefreshToken = () => {
|
||||
return <button onClick={() => refreshFunction()}>Custom Refresh</button>
|
||||
}
|
||||
export default RefreshToken
|
||||
16
test/server-functions/components/refreshFunction.tsx
Normal file
16
test/server-functions/components/refreshFunction.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use server'
|
||||
|
||||
import { refresh } from '@payloadcms/next/auth'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
export async function refreshFunction() {
|
||||
try {
|
||||
return await refresh({
|
||||
collection: 'users', // update this to your collection slug
|
||||
config,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
34
test/server-functions/config.ts
Normal file
34
test/server-functions/config.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [],
|
||||
admin: {
|
||||
autoLogin: false,
|
||||
components: {
|
||||
beforeLogin: ['/components/login.js'],
|
||||
header: ['/components/refresh.js', '/components/logout.js'],
|
||||
},
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
97
test/server-functions/e2e.spec.ts
Normal file
97
test/server-functions/e2e.spec.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { devUser } from 'credentials.js'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||
import type { Config } from './payload-types.js'
|
||||
|
||||
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
let payload: PayloadTestSDK<Config>
|
||||
|
||||
const { beforeAll, describe } = test
|
||||
|
||||
describe('Server Functions', () => {
|
||||
let page: Page
|
||||
let url: AdminUrlUtil
|
||||
let serverURL: string
|
||||
|
||||
beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
|
||||
url = new AdminUrlUtil(serverURL, 'users')
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
|
||||
await ensureCompilationIsDone({
|
||||
page,
|
||||
serverURL,
|
||||
noAutoLogin: true,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Auth functions', () => {
|
||||
test('should log user in from login server function', async () => {
|
||||
await page.goto(`${serverURL}/admin`)
|
||||
|
||||
// Expect email and password fields to be visible
|
||||
await expect(page.locator('#email')).toBeVisible()
|
||||
await expect(page.locator('#password')).toBeVisible()
|
||||
|
||||
await page.fill('#email', devUser.email)
|
||||
await page.fill('#password', devUser.password)
|
||||
|
||||
const loginButton = page.locator('text=Custom Login')
|
||||
await expect(loginButton).toBeVisible()
|
||||
await loginButton.click()
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
await page.reload()
|
||||
await page.goto(`${serverURL}/admin/account`)
|
||||
await expect(page.locator('h1[title="dev@payloadcms.com"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should refresh user from refresh server function', async () => {
|
||||
await page.goto(`${serverURL}/admin`)
|
||||
|
||||
const initialCookie = await page.context().cookies()
|
||||
const payloadToken = initialCookie.find((cookie) => cookie.name === 'payload-token')
|
||||
expect(payloadToken).toBeDefined()
|
||||
const initialExpiry = payloadToken?.expires
|
||||
|
||||
const refreshButton = page.locator('text=Custom Refresh')
|
||||
await expect(refreshButton).toBeVisible()
|
||||
await refreshButton.click()
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
const updatedCookie = await page.context().cookies()
|
||||
const updatedPayloadToken = updatedCookie.find((cookie) => cookie.name === 'payload-token')
|
||||
expect(updatedPayloadToken).toBeDefined()
|
||||
expect(updatedPayloadToken?.expires).not.toBe(initialExpiry)
|
||||
})
|
||||
|
||||
test('should log user out from logout server function', async () => {
|
||||
await page.goto(`${serverURL}/admin`)
|
||||
const logoutButton = page.locator('text=Custom Logout')
|
||||
await expect(logoutButton).toBeVisible()
|
||||
await logoutButton.click()
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
await page.reload()
|
||||
await page.goto(`${serverURL}/admin`)
|
||||
await expect(page.locator('#email')).toBeVisible()
|
||||
await expect(page.locator('#password')).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
19
test/server-functions/eslint.config.js
Normal file
19
test/server-functions/eslint.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { rootParserOptions } from '../../eslint.config.js'
|
||||
import { testEslintConfig } from '../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...testEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
242
test/server-functions/payload-types.ts
Normal file
242
test/server-functions/payload-types.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null;
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
13
test/server-functions/tsconfig.eslint.json
Normal file
13
test/server-functions/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// extend your base config to share compilerOptions, etc
|
||||
//"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// ensure that nobody can accidentally use this config for a build
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
// whatever paths you intend to lint
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
]
|
||||
}
|
||||
8
test/server-functions/tsconfig.json
Normal file
8
test/server-functions/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@payload-config": ["./config.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user