-
Notifications
You must be signed in to change notification settings - Fork 0
feat(cms): add public read access and draft mode support #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ff06a7b
36e201c
fa42c07
3ea3584
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is a needed change for the other tickets, then we can move this file change to a separate PR, or move the change for the |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,31 @@ | ||
| import type { Access } from 'payload' | ||
|
|
||
| export const anyone: Access = () => true | ||
| export const anyone: Access = ({ req: { user, headers, query, routeParams, pathname } }) => { | ||
| if (user) { | ||
| return true | ||
| } | ||
|
|
||
| const authorization = headers.get('authorization') | ||
| if ( | ||
| authorization && | ||
| process.env.DRAFT_SECRET_TOKEN && | ||
| authorization === `Bearer ${process.env.DRAFT_SECRET_TOKEN}` | ||
| ) { | ||
| return true | ||
| } | ||
|
|
||
| const isGlobalRequest = | ||
| typeof routeParams?.global === 'string' || | ||
| (typeof pathname === 'string' && pathname.includes('/api/globals/')) | ||
|
|
||
| // Global reads should return boolean access, not a where clause. | ||
| if (isGlobalRequest) { | ||
| const draftParam = query?.draft | ||
| const isDraftRequested = draftParam === true || draftParam === 'true' | ||
| return !isDraftRequested | ||
| } | ||
|
|
||
| return { | ||
| or: [{ _status: { equals: 'published' } }, { _status: { exists: false } }], | ||
| } | ||
| } |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's revisit this for endpoint once all models for the durianpy website CMS integration is done, so we can check if all types are generated properly |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| import fs from 'fs' | ||
| import path from 'path' | ||
| import { checkResourceAccess } from '@/access/checkResourceAccess' | ||
| import { SIDEBAR_GROUPS, getSidebarGroupItems } from '@/constants/sidebarGroup' | ||
|
|
||
| import { APIError, type Endpoint } from 'payload' | ||
|
|
||
| const GROUP_SLUG = SIDEBAR_GROUPS.DURIANPY_WEBSITE | ||
|
|
||
| function getCommentStartBefore(content: string, index: number): number { | ||
| const commentEnd = content.lastIndexOf('*/', index) | ||
| if (commentEnd === -1) return index | ||
|
|
||
| const commentStart = content.lastIndexOf('/**', commentEnd) | ||
| if (commentStart === -1) return index | ||
|
|
||
| const between = content.slice(commentEnd + 2, index) | ||
| return /^\s*$/.test(between) ? commentStart : index | ||
| } | ||
|
|
||
| function extractInterfaceBlock(content: string, interfaceName: string): string | null { | ||
| const declaration = `export interface ${interfaceName}` | ||
| const declarationIndex = content.indexOf(declaration) | ||
|
|
||
| if (declarationIndex === -1) return null | ||
|
|
||
| const startIndex = getCommentStartBefore(content, declarationIndex) | ||
| const braceStart = content.indexOf('{', declarationIndex) | ||
|
|
||
| if (braceStart === -1) return null | ||
|
|
||
| let braceDepth = 0 | ||
| for (let i = braceStart; i < content.length; i += 1) { | ||
| const char = content[i] | ||
|
|
||
| if (char === '{') braceDepth += 1 | ||
| if (char === '}') braceDepth -= 1 | ||
|
|
||
| if (braceDepth === 0) { | ||
| return content.slice(startIndex, i + 1) | ||
| } | ||
| } | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| function getTypeMapFromConfig(typesFileContent: string): Map<string, string> { | ||
| const map = new Map<string, string>() | ||
|
|
||
| const extractEntriesFromBlock = (blockMatch: RegExpMatchArray | null) => { | ||
| if (!blockMatch) return | ||
| const blockContent = blockMatch[1] | ||
| const entryRegex = /^\s*(?:'([^']+)'|([A-Za-z0-9_-]+)):\s*([A-Za-z0-9_]+);\s*$/gm | ||
| let entryMatch: RegExpExecArray | null | ||
| while ((entryMatch = entryRegex.exec(blockContent)) !== null) { | ||
| const slug = entryMatch[1] ?? entryMatch[2] | ||
| const typeName = entryMatch[3] | ||
| if (slug && typeName) { | ||
| map.set(slug, typeName) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extractEntriesFromBlock(typesFileContent.match(/collections:\s*\{([\s\S]*?)\n\s*\};/)) | ||
| extractEntriesFromBlock(typesFileContent.match(/globals:\s*\{([\s\S]*?)\n\s*\};/)) | ||
|
|
||
| return map | ||
| } | ||
|
|
||
| export const durianpyWebsiteTypesEndpoint: Endpoint = { | ||
| method: 'get', | ||
| path: '/durianpy-website-types', | ||
| handler: async (req) => { | ||
| if (!req.user && req.payload) { | ||
| const { user } = await req.payload.auth({ headers: req.headers }) | ||
| req.user = user | ||
| } | ||
|
|
||
| if (!req.user) { | ||
| throw new APIError('Unauthorized', 401) | ||
| } | ||
|
|
||
| const hasAccess = checkResourceAccess({ req }, GROUP_SLUG, 'read') | ||
| if (!hasAccess) { | ||
| throw new APIError('Forbidden', 403) | ||
| } | ||
|
|
||
| try { | ||
| const fullTypes = fs.readFileSync( | ||
| path.resolve(process.cwd(), 'src/payload-types.ts'), | ||
| 'utf-8', | ||
| ) | ||
|
|
||
| const groupResourceSlugs = [...getSidebarGroupItems(GROUP_SLUG)].sort((a, b) => | ||
| a.localeCompare(b), | ||
| ) | ||
|
|
||
| const resourceTypeMap = getTypeMapFromConfig(fullTypes) | ||
|
|
||
| const missingTypeMappings: string[] = [] | ||
| const selectedTypeNames = groupResourceSlugs | ||
| .map((slug) => { | ||
| const typeName = resourceTypeMap.get(slug) | ||
| if (!typeName) missingTypeMappings.push(slug) | ||
| return typeName | ||
| }) | ||
| .filter((name): name is string => Boolean(name)) | ||
|
|
||
| if (missingTypeMappings.length > 0) { | ||
| return new Response( | ||
| JSON.stringify({ | ||
| error: 'Missing resource to type mappings in payload-types.ts', | ||
| group: GROUP_SLUG, | ||
| missingResourceSlugs: missingTypeMappings, | ||
| }), | ||
| { | ||
| status: 500, | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| const missingInterfaceBlocks: string[] = [] | ||
| const extractedInterfaces = selectedTypeNames | ||
| .map((typeName) => { | ||
| const block = extractInterfaceBlock(fullTypes, typeName) | ||
| if (!block) missingInterfaceBlocks.push(typeName) | ||
| return block | ||
| }) | ||
| .filter((block): block is string => Boolean(block)) | ||
|
|
||
| if (missingInterfaceBlocks.length > 0) { | ||
| return new Response( | ||
| JSON.stringify({ | ||
| error: 'Could not extract one or more interfaces from payload-types.ts', | ||
| group: GROUP_SLUG, | ||
| missingInterfaces: missingInterfaceBlocks, | ||
| }), | ||
| { | ||
| status: 500, | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| const typeSyncHeader = `// Auto-generated from src/payload-types.ts\n// Group: ${GROUP_SLUG}\n// Resources: ${groupResourceSlugs.join(', ')}` | ||
| const payload = `${typeSyncHeader}\n\n${extractedInterfaces.join('\n\n')}` | ||
|
|
||
| return new Response(payload, { | ||
| headers: { | ||
| 'Content-Type': 'text/plain; charset=utf-8', | ||
| 'Cache-Control': 'no-store', | ||
| }, | ||
| }) | ||
| } catch (error) { | ||
| if (error instanceof APIError) { | ||
| throw error | ||
| } | ||
| throw new APIError( | ||
| error instanceof Error | ||
| ? error.message | ||
| : 'Failed to generate durianpy-website type sync payload', | ||
| 500, | ||
| ) | ||
| } | ||
| }, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ASPactores Do you think we should add some sort of authorizer here to check the tokens?