Files
payloadcms/test/helpers/NextRESTClient.ts
Alessio Gravili a7b0f8ba36 feat!: new server-only, faster and immediate autoLogin (#7224)
- When autoLogin is enabled, it will no longer flash an unresponsive
"login" screen. Instead, it will straight up open the admin panel.
That's because, on the server, we will now always & immediately see the
user as authenticated, thus no initial login view is pushed to the
client until the client component sends the auth request anymore. Less
useless requests. Additionally, jwt verification is now completely
skipped
- No more auto-login related frontend code. autoLogin handling has been
removed from the frontend `Auth` component
- less code to maintain, this is way simpler now

**For reviewers:**
- The new logic for autoFill without prefillOnly is here: [jwt auth
strategy](https://github.com/payloadcms/payload/pull/7224/files#diff-7d40839079a8b2abb58233e5904513ab321023a70538229dfaf1dfee067dc8bfR21)
- The new logic for autoFill with prefillOnly is here: [Server Login
View](https://github.com/payloadcms/payload/pull/7224/files#diff-683770104f196196743398a698fbf8987f00e4426ca1c0ace3658d18ab80e82dL72)
=> [Client Login
Form](https://github.com/payloadcms/payload/pull/7224/files#diff-ac3504d3b3b0489455245663649bef9e84477bf0c1185da5a4d3a612450f01eeL20)

**BREAKING**
`autoLogin` without `prefillOnly` set now also affects graphQL/Rest
operations. Only the user specified in `autoLogin` will be returned.
Within the graphQL/Rest/Local API, this should still allow you to
authenticate with a different user, as the autoLogin user is only used
if no token is set.
2024-07-20 23:25:50 +00:00

209 lines
5.7 KiB
TypeScript

import type { SanitizedConfig, Where } from 'payload'
import type { ParsedQs } from 'qs-esm'
import {
REST_DELETE as createDELETE,
REST_GET as createGET,
GRAPHQL_POST as createGraphqlPOST,
REST_PATCH as createPATCH,
REST_POST as createPOST,
} from '@payloadcms/next/routes'
import * as qs from 'qs-esm'
import { devUser } from '../credentials.js'
type ValidPath = `/${string}`
type RequestOptions = {
auth?: boolean
query?: {
depth?: number
fallbackLocale?: string
limit?: number
locale?: string
page?: number
sort?: string
where?: Where
}
}
type FileArg = {
file?: Omit<File, 'webkitRelativePath'>
}
function generateQueryString(query: RequestOptions['query'], params: ParsedQs): string {
return qs.stringify(
{
...(params || {}),
...(query || {}),
},
{
addQueryPrefix: true,
},
)
}
export class NextRESTClient {
private _DELETE: (request: Request, args: { params: { slug: string[] } }) => Promise<Response>
private _GET: (request: Request, args: { params: { slug: string[] } }) => Promise<Response>
private _GRAPHQL_POST: (request: Request) => Promise<Response>
private _PATCH: (request: Request, args: { params: { slug: string[] } }) => Promise<Response>
private _POST: (request: Request, args: { params: { slug: string[] } }) => Promise<Response>
private readonly config: SanitizedConfig
private token: string
serverURL: string = 'http://localhost:3000'
constructor(config: SanitizedConfig) {
this.config = config
if (config?.serverURL) this.serverURL = config.serverURL
this._GET = createGET(config)
this._POST = createPOST(config)
this._DELETE = createDELETE(config)
this._PATCH = createPATCH(config)
this._GRAPHQL_POST = createGraphqlPOST(config)
}
private buildHeaders(options: FileArg & RequestInit & RequestOptions): Headers {
const defaultHeaders = {
'Content-Type': 'application/json',
}
const headers = new Headers({
...(options?.file
? {
'Content-Length': options.file.size.toString(),
}
: defaultHeaders),
...(options?.headers || {}),
})
if (options.auth !== false && this.token) {
headers.set('Authorization', `JWT ${this.token}`)
}
if (options.auth === false) {
headers.set('DisableAutologin', 'true')
}
return headers
}
private generateRequestParts(path: ValidPath): {
params?: ParsedQs
slug: string[]
url: string
} {
const [slugs, params] = path.slice(1).split('?')
const url = `${this.serverURL}${this.config.routes.api}/${slugs}`
return {
slug: slugs.split('/'),
params: params ? qs.parse(params) : undefined,
url,
}
}
async DELETE(path: ValidPath, options: RequestInit & RequestOptions = {}): Promise<Response> {
const { slug, params, url } = this.generateRequestParts(path)
const { query, ...rest } = options || {}
const queryParams = generateQueryString(query, params)
const request = new Request(`${url}${queryParams}`, {
...rest,
headers: this.buildHeaders(options),
method: 'DELETE',
})
return this._DELETE(request, { params: { slug } })
}
async GET(
path: ValidPath,
options: Omit<RequestInit, 'body'> & RequestOptions = {},
): Promise<Response> {
const { slug, params, url } = this.generateRequestParts(path)
const { query, ...rest } = options || {}
const queryParams = generateQueryString(query, params)
const request = new Request(`${url}${queryParams}`, {
...rest,
headers: this.buildHeaders(options),
method: 'GET',
})
return this._GET(request, { params: { slug } })
}
async GRAPHQL_POST(options: RequestInit & RequestOptions): Promise<Response> {
const { query, ...rest } = options
const queryParams = generateQueryString(query, {})
const request = new Request(
`${this.serverURL}${this.config.routes.api}${this.config.routes.graphQL}${queryParams}`,
{
...rest,
headers: this.buildHeaders(options),
method: 'POST',
},
)
return this._GRAPHQL_POST(request)
}
async PATCH(path: ValidPath, options: FileArg & RequestInit & RequestOptions): Promise<Response> {
const { slug, params, url } = this.generateRequestParts(path)
const { query, ...rest } = options
const queryParams = generateQueryString(query, params)
const request = new Request(`${url}${queryParams}`, {
...rest,
headers: this.buildHeaders(options),
method: 'PATCH',
})
return this._PATCH(request, { params: { slug } })
}
async POST(
path: ValidPath,
options: FileArg & RequestInit & RequestOptions = {},
): Promise<Response> {
const { slug, params, url } = this.generateRequestParts(path)
const queryParams = generateQueryString({}, params)
const request = new Request(`${url}${queryParams}`, {
...options,
headers: this.buildHeaders(options),
method: 'POST',
})
return this._POST(request, { params: { slug } })
}
async login({
slug,
credentials,
}: {
credentials?: {
email: string
password: string
}
slug: string
}): Promise<{ [key: string]: any }> {
const response = await this.POST(`/${slug}/login`, {
body: JSON.stringify(
credentials ? { ...credentials } : { email: devUser.email, password: devUser.password },
),
})
const result = await response.json()
this.token = result.token
if (!result.token) {
// If the token is not in the response body, then we can extract it from the cookies
const setCookie = response.headers.get('Set-Cookie')
const tokenMatchResult = setCookie?.match(/payload-token=(?<token>.+?);/)
this.token = tokenMatchResult?.groups?.token
}
return result
}
}