Skip to content

Commit d5eaa47

Browse files
committed
feat: own action blocks & sections & render action-detail page
1 parent 6d63de7 commit d5eaa47

30 files changed

Lines changed: 1119 additions & 285 deletions

media/example-action.json

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
{
2+
"identifier": "example-action",
3+
"name": [
4+
{
5+
"code": "en-US",
6+
"content": "Example Action"
7+
}
8+
],
9+
"documentation": "A simple example action",
10+
"author": "code0-tech",
11+
"icon": "tabler:bolt",
12+
"version": "0.0.0",
13+
"flowTypes": [
14+
{
15+
"identifier": "user_created",
16+
"name": [
17+
{
18+
"code": "en-US",
19+
"content": "User created event"
20+
}
21+
],
22+
"description": [
23+
{
24+
"code": "en-US",
25+
"content": "Triggers on user creation and has a payload including the user database id"
26+
}
27+
],
28+
"displayMessage": [
29+
{
30+
"code": "en-US",
31+
"content": "Triggers on user creation"
32+
}
33+
],
34+
"version": "0.0.0",
35+
"displayIcon": "tabler:note",
36+
"definitionSource": "action",
37+
"signature": "(): {userId: number}",
38+
"runtimeIdentifier": "user_created"
39+
}
40+
],
41+
"runtimeFlowTypes": [
42+
{
43+
"identifier": "user_created",
44+
"name": [
45+
{
46+
"code": "en-US",
47+
"content": "User created event"
48+
}
49+
],
50+
"description": [
51+
{
52+
"code": "en-US",
53+
"content": "Triggers on user creation and has a payload including the user database id"
54+
}
55+
],
56+
"displayMessage": [
57+
{
58+
"code": "en-US",
59+
"content": "Triggers on user creation"
60+
}
61+
],
62+
"version": "0.0.0",
63+
"displayIcon": "tabler:note",
64+
"definitionSource": "action",
65+
"signature": "(): {userId: number}"
66+
}
67+
],
68+
"functionDefinitions": [
69+
{
70+
"runtimeName": "fibonacci",
71+
"parameterDefinitions": [
72+
{
73+
"runtimeName": "test",
74+
"defaultValue": {
75+
"numberValue": {
76+
"integer": "10"
77+
}
78+
},
79+
"optional": false,
80+
"hidden": false,
81+
"name": [
82+
{
83+
"code": "en-US",
84+
"content": "Input Number"
85+
}
86+
],
87+
"description": [
88+
{
89+
"code": "en-US",
90+
"content": "The position in the Fibonacci sequence"
91+
}
92+
],
93+
"runtimeDefinitionName": "test"
94+
}
95+
],
96+
"signature": "(test: number): number",
97+
"name": [
98+
{
99+
"code": "en-US",
100+
"content": "Compute Fibonacci Number"
101+
}
102+
],
103+
"displayMessage": [
104+
{
105+
"code": "en-US",
106+
"content": "Computes the n-th Fibonacci number"
107+
}
108+
],
109+
"version": "0.0.0",
110+
"displayIcon": "tabler:note",
111+
"definitionSource": "action",
112+
"runtimeDefinitionName": "fibonacci_runtime"
113+
}
114+
],
115+
"runtimeFunctionDefinitions": [
116+
{
117+
"runtimeName": "fibonacci_runtime",
118+
"runtimeParameterDefinitions": [
119+
{
120+
"runtimeName": "test",
121+
"optional": false,
122+
"hidden": false,
123+
"name": [
124+
{
125+
"code": "en-US",
126+
"content": "N"
127+
}
128+
]
129+
}
130+
],
131+
"signature": "(test: number): number",
132+
"name": [
133+
{
134+
"code": "en-US",
135+
"content": "Fibonacci (Runtime)"
136+
}
137+
],
138+
"displayMessage": [
139+
{
140+
"code": "en-US",
141+
"content": "Computes the n-th Fibonacci number"
142+
}
143+
],
144+
"version": "0.0.0",
145+
"displayIcon": "tabler:note",
146+
"definitionSource": "action"
147+
}
148+
],
149+
"definitionDataTypes": [
150+
{
151+
"identifier": "email_address",
152+
"name": [
153+
{
154+
"code": "en-US",
155+
"content": "Email Address"
156+
}
157+
],
158+
"rules": [
159+
{
160+
"regex": {
161+
"pattern": "^[^@]+@[^@]+\\.[^@]+$"
162+
}
163+
}
164+
],
165+
"type": "string",
166+
"version": "0.0.0",
167+
"definitionSource": "action"
168+
}
169+
],
170+
"configurations": [
171+
{
172+
"identifier": "EXAMPLE_CONFIG",
173+
"name": [
174+
{
175+
"code": "en-US",
176+
"content": "Example Config"
177+
}
178+
],
179+
"type": "string",
180+
"optional": false,
181+
"hidden": false
182+
}
183+
]
184+
}

src/app/(landing)/[locale]/actions/[slug]/page.tsx

Lines changed: 5 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,35 @@
1-
import { ActionIcon } from "@/components/ActionIcon"
2-
import { ActionTriggerCard } from "@/components/ActionTriggerCard"
3-
import { ActionCard } from "@/components/cards/ActionCard"
1+
import { PageBlocks } from "@/components/PageBlockRenderer"
42
import { Aurora } from "@/components/ui/Aurora"
53
import { LandingContainer } from "@/components/ui/LandingContainer"
6-
import { LinkButton } from "@/components/ui/LinkButton"
4+
import { fetchMediaJson } from "@/lib/actionExtraction"
75
import { getPageLocaleAndSlug, type LocaleSlugPageParams } from "@/lib/appRoute"
8-
import { extractFlowTypesFromJson, extractFunctionDefinitionsFromJson, fetchMediaJson } from "@/lib/actionExtraction"
96
import { getActionBySlug, getLandingPage } from "@/lib/cms"
10-
import { findPageBlock } from "@/lib/pageBlocks"
117
import { createMetadata } from "@/lib/siteConfig"
128
import type { Media } from "@/payload-types"
13-
import { IconArrowLeft } from "@tabler/icons-react"
149
import type { Metadata } from "next"
1510
import { notFound } from "next/navigation"
16-
import type { ReactNode } from "react"
1711

1812
export default async function ActionDetailPage({ params }: { params: LocaleSlugPageParams }) {
1913
const { locale, slug } = await getPageLocaleAndSlug(params)
2014
const action = await getActionBySlug(slug, locale)
2115
if (!action) notFound()
22-
const actionsPage = await getLandingPage("actions", locale)
16+
const actionsPage = await getLandingPage("action-details", locale)
2317

2418
const module = action.module as Media | undefined
2519
const moduleJson = await fetchMediaJson(module).catch(() => null)
26-
const extractedFlowTypes = extractFlowTypesFromJson(moduleJson)
27-
const extractedFunctionDefinitions = extractFunctionDefinitionsFromJson(moduleJson)
2820
const references = (action.references ?? []).filter((reference): reference is Exclude<typeof reference, number> => typeof reference !== "number")
2921
const tags = (action.tags ?? []).filter((tag): tag is string => Boolean(tag))
30-
const actionsBlock = findPageBlock(actionsPage, "actions")
31-
const referencesLabel = actionsBlock?.referencesLabel ?? (locale === "de" ? "Referenzen" : "References")
32-
const flowTypesLabel = actionsBlock?.flowTypesLabel ?? "FlowTypes"
33-
const functionDefinitionsLabel = actionsBlock?.functionDefinitionsLabel ?? "FunctionDefinitions"
3422

3523
return (
3624
<>
3725
<Aurora />
38-
<LandingContainer className="pt-32">
39-
<div className="mx-auto flex w-full max-w-4xl flex-col gap-8">
40-
<LinkButton href={`/${locale}/actions`} showArrow={false} className="border-0 hover:bg-white/10 pl-2.5 pr-4 py-1 rounded-xl hover:text-white after:hidden">
41-
<IconArrowLeft size={16} />
42-
{locale === "de" ? "Zurück" : "Back"}
43-
</LinkButton>
44-
45-
<div className="flex flex-col gap-8">
46-
<div className="relative z-10 flex flex-col gap-12">
47-
<div className="flex flex-col gap-4 sm:flex-row sm:justify-between">
48-
<div className="flex flex-col gap-4 sm:flex-row sm:items-start">
49-
{action.icon && (
50-
<div className="flex size-20 shrink-0 items-center justify-center overflow-hidden rounded-3xl border border-white/10 bg-primary text-white">
51-
<ActionIcon icon={action.icon} size={40} />
52-
</div>
53-
)}
54-
55-
<div className="flex flex-col min-w-0 flex-1 gap-2">
56-
<h1 className="mt-1 text-3xl font-semibold tracking-tight text-white sm:text-4xl">{action.title}</h1>
57-
{action.description && <div className="max-w-3xl whitespace-pre-line text-sm leading-6 text-secondary">{action.description}</div>}
58-
</div>
59-
</div>
60-
</div>
61-
62-
{(extractedFlowTypes.length > 0 || extractedFunctionDefinitions.length > 0) && (
63-
<div className="space-y-8">
64-
{extractedFlowTypes.length > 0 && (
65-
<ActionDefinitionGroup label={flowTypesLabel}>
66-
{extractedFlowTypes.map((item) => (
67-
<ActionTriggerCard key={item.id} type="flowType" item={item} />
68-
))}
69-
</ActionDefinitionGroup>
70-
)}
71-
72-
{extractedFunctionDefinitions.length > 0 && (
73-
<ActionDefinitionGroup label={functionDefinitionsLabel}>
74-
{extractedFunctionDefinitions.map((item) => (
75-
<ActionTriggerCard key={item.id} type="functionDefinition" item={item} />
76-
))}
77-
</ActionDefinitionGroup>
78-
)}
79-
</div>
80-
)}
81-
82-
{references.length > 0 && (
83-
<div className="space-y-4">
84-
<p className="text-sm text-tertiary">{referencesLabel}</p>
85-
<div className="grid gap-4 md:grid-cols-2">
86-
{references.map((reference) => (
87-
<ActionCard key={reference.id} action={reference} locale={locale} />
88-
))}
89-
</div>
90-
</div>
91-
)}
92-
</div>
93-
</div>
94-
</div>
26+
<LandingContainer className="pt-16">
27+
<PageBlocks blocks={actionsPage?.layout} locale={locale} action={action} actionModuleJson={moduleJson} actionReferences={references as (typeof action)[]} />
9528
</LandingContainer>
9629
</>
9730
)
9831
}
9932

100-
function ActionDefinitionGroup({ label, children }: { label: string; children: ReactNode }) {
101-
return (
102-
<div className="space-y-4">
103-
<p className="shrink-0 text-sm text-tertiary">{label}</p>
104-
<div className="flex flex-col gap-3">{children}</div>
105-
</div>
106-
)
107-
}
108-
10933
export async function generateMetadata({ params }: { params: LocaleSlugPageParams }): Promise<Metadata> {
11034
const { locale, slug } = await params
11135
if (!slug?.trim()) return createMetadata()

src/app/(landing)/globals.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@
8282
--animate-cell-ripple: cell-ripple var(--cell-ripple-duration, 200ms) ease-out none 1 var(--cell-ripple-delay, 0ms);
8383
}
8484

85+
@keyframes action-hero-pulse {
86+
0%, 100% { left: -60%; opacity: 0; }
87+
15% { opacity: 1; }
88+
85% { opacity: 1; }
89+
100% { left: 100%; opacity: 0; }
90+
}
91+
8592
@keyframes cell-ripple {
8693
0% {
8794
opacity: 0.4;

src/app/api/actions/import/route.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { extractActionModuleInfo } from "@/lib/actionExtraction"
22
import { getPayloadClient } from "@/lib/payloadClient"
33
import type { Action } from "@/payload-types"
4+
import { createHmac, timingSafeEqual } from "node:crypto"
45
import { NextResponse } from "next/server"
56

67
function getBearerToken(request: Request) {
@@ -10,6 +11,25 @@ function getBearerToken(request: Request) {
1011
return authorization.slice("bearer ".length).trim()
1112
}
1213

14+
function verifyJwt(token: string | null, secret: string) {
15+
if (!token) return false
16+
const parts = token.split(".")
17+
if (parts.length !== 3) return false
18+
19+
const [encodedHeader, encodedPayload, signature] = parts
20+
try {
21+
const header = JSON.parse(Buffer.from(encodedHeader, "base64url").toString("utf8")) as { alg?: string; typ?: string }
22+
const payload = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8")) as { exp?: number; nbf?: number }
23+
if (header.alg !== "HS256" || header.typ !== "JWT") return false
24+
const expectedSignature = createHmac("sha256", secret).update(`${encodedHeader}.${encodedPayload}`).digest("base64url")
25+
if (signature.length !== expectedSignature.length || !timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) return false
26+
const now = Math.floor(Date.now() / 1000)
27+
return !(typeof payload.exp === "number" && payload.exp <= now) && !(typeof payload.nbf === "number" && payload.nbf > now)
28+
} catch {
29+
return false
30+
}
31+
}
32+
1333
function createJsonUploadFile(identifier: string, module: unknown) {
1434
const json = JSON.stringify(module, null, 2)
1535
return {
@@ -24,7 +44,10 @@ export async function POST(request: Request) {
2444
const expectedToken = process.env.ACTIONS_IMPORT_SECRET?.trim()
2545
const receivedToken = getBearerToken(request)
2646

27-
if (!expectedToken || receivedToken !== expectedToken) {
47+
const isValidJwt = verifyJwt(receivedToken, expectedToken ?? "")
48+
const isDevelopmentSecret = process.env.NODE_ENV === "development" && receivedToken === expectedToken
49+
50+
if (!expectedToken || (!isValidJwt && !isDevelopmentSecret)) {
2851
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
2952
}
3053

0 commit comments

Comments
 (0)