Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1122acd5a5 |
@@ -1,324 +0,0 @@
|
|||||||
---
|
|
||||||
name: Coolify React CMS Stack
|
|
||||||
overview: "Empfohlener Standard-Stack: Next.js (React) + Payload CMS (self-hosted) + PostgreSQL, alles als Docker-Images über GitHub → Coolify. Cursor/MCP für die Entwicklung, GitHub Actions für Qualitätssicherung vor dem Deploy."
|
|
||||||
todos:
|
|
||||||
- id: scaffold-monorepo
|
|
||||||
content: "Greenfield-Monorepo anlegen: apps/web (Next standalone), apps/cms (Payload), packages/shared-types"
|
|
||||||
status: pending
|
|
||||||
- id: docker-coolify
|
|
||||||
content: "Dockerfiles, docker-compose.yml, docs/COOLIFY.md (3 Services: Postgres, CMS, Web) nach FlixCooks-Muster"
|
|
||||||
status: pending
|
|
||||||
- id: cms-content-model
|
|
||||||
content: Payload Collections + Webhooks für On-Demand Revalidation; Env-Beispiele in .env.example
|
|
||||||
status: pending
|
|
||||||
- id: github-ci
|
|
||||||
content: "GitHub Actions: lint, typecheck, build, docker build smoke; branch protection auf main"
|
|
||||||
status: pending
|
|
||||||
- id: mcp-agents
|
|
||||||
content: .cursor/mcp.json + .agents/brain.md mit Stack-, Env- und Deploy-Regeln für KI
|
|
||||||
status: pending
|
|
||||||
- id: animation-baseline
|
|
||||||
content: "GSAP/Lenis/Framer-Baseline-Komponenten und Regel: Overlays stoppen Lenis"
|
|
||||||
status: pending
|
|
||||||
isProject: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# Tech-Stack & Workflow: React-Sites, CMS, Docker, Coolify
|
|
||||||
|
|
||||||
## Empfehlung (Default)
|
|
||||||
|
|
||||||
Du bist unsicher bei CMS und Frontend – hier ist ein **bewährtes Default**, das zu deinen Vorgaben passt (React, Animationen, Docker, Coolify, KI/MCP, CI):
|
|
||||||
|
|
||||||
| Schicht | Technologie | Warum |
|
|
||||||
|---------|-------------|--------|
|
|
||||||
| **Frontend** | [Next.js 15](https://nextjs.org) (App Router, TypeScript) | SEO/SSR/ISR, React-Ökosystem, `output: 'standalone'` für ein schlankes Production-Docker-Image |
|
|
||||||
| **Animationen** | GSAP (+ ScrollTrigger), Framer Motion, Lenis | GSAP für Scroll/Timeline-Premium-Feel; Framer für UI-Micro-Interactions; Lenis kennst du bereits aus FlixCooks |
|
|
||||||
| **CMS** | [Payload CMS 3](https://payloadcms.com) (eigener Container) | TypeScript, Postgres-native, Admin-UI out of the box, Docker-freundlich, passt zu Coolify wie dein aktuelles Postgres-Setup |
|
|
||||||
| **Datenbank** | PostgreSQL 16 (Coolify Database Service) | Eine Instanz, getrennte DBs/User für CMS vs. App optional |
|
|
||||||
| **Runtime / Deploy** | Docker + [Coolify](https://coolify.io) | Git-Webhook → Build → Traefik/HTTPS; du hast das Muster schon in [docs/COOLIFY.md](docs/COOLIFY.md) |
|
|
||||||
| **Lokale Dev** | `docker compose` (Web + CMS + Postgres) | Parität zu Production, wie [docker-compose.yml](docker-compose.yml) bei FlixCooks |
|
|
||||||
| **KI-Entwicklung** | Cursor + MCP-Server | Repo-Kontext, GitHub, Docs, optional DB |
|
|
||||||
| **CI** | GitHub Actions | Lint, Types, Build, Docker-Smoke, optional E2E; PR-Review wie [.github/workflows/gemini-pr-review.yml](.github/workflows/gemini-pr-review.yml) |
|
|
||||||
|
|
||||||
**Alternative Frontend:** Vite + React SPA + nginx-Image – maximal frei für reine Animation-Landingpages, aber schlechteres SEO und kein ISR ohne Extra-Aufwand. **Alternative CMS (weniger Ops):** Sanity/Contentful (Cloud) – nur Frontend-Container auf Coolify. **Alternative CMS (kein Backend):** Tina/Decap + Markdown im Repo – gut für Blogs, schwächer für Redakteur:innen ohne Git.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Zielarchitektur auf Coolify
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
subgraph dev [Entwicklung]
|
|
||||||
Cursor[Cursor + MCP]
|
|
||||||
LocalCompose[docker compose]
|
|
||||||
Cursor --> LocalCompose
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph github [GitHub]
|
|
||||||
Repo[Monorepo]
|
|
||||||
GHA[GitHub Actions CI]
|
|
||||||
Repo --> GHA
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph coolify [Coolify Server]
|
|
||||||
PG[(PostgreSQL)]
|
|
||||||
CMS[Payload CMS Container]
|
|
||||||
WEB[Next.js Container]
|
|
||||||
Traefik[Traefik HTTPS]
|
|
||||||
PG --> CMS
|
|
||||||
CMS -->|REST/GraphQL| WEB
|
|
||||||
Traefik --> WEB
|
|
||||||
Traefik --> CMS
|
|
||||||
end
|
|
||||||
|
|
||||||
dev -->|push main| Repo
|
|
||||||
GHA -->|grüner Build| Repo
|
|
||||||
Repo -->|Webhook Deploy| coolify
|
|
||||||
```
|
|
||||||
|
|
||||||
**Drei Coolify-Ressourcen** (analog zu deinem FlixCooks-Setup: Postgres + App):
|
|
||||||
|
|
||||||
1. **PostgreSQL** – internal URL, nicht öffentlich
|
|
||||||
2. **CMS-App** – Dockerfile aus `apps/cms`, Port z. B. 3001, Env: `DATABASE_URL`, `PAYLOAD_SECRET`
|
|
||||||
3. **Web-App** – Dockerfile aus `apps/web`, Port 3000, Env: `CMS_URL` (internal), `REVALIDATE_SECRET` für On-Demand-ISR
|
|
||||||
|
|
||||||
Persistenz: Postgres-Volume (Inhalte), optional Volume für CMS-Uploads (`/app/media`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Repository-Struktur (Greenfield-Vorlage)
|
|
||||||
|
|
||||||
Ein Repo pro „Site-Familie“ oder Monorepo für mehrere Marken:
|
|
||||||
|
|
||||||
```
|
|
||||||
my-site/
|
|
||||||
├── apps/
|
|
||||||
│ ├── web/ # Next.js
|
|
||||||
│ │ ├── Dockerfile
|
|
||||||
│ │ ├── src/
|
|
||||||
│ │ └── next.config.ts # output: 'standalone'
|
|
||||||
│ └── cms/ # Payload
|
|
||||||
│ ├── Dockerfile
|
|
||||||
│ └── payload.config.ts
|
|
||||||
├── packages/
|
|
||||||
│ └── shared-types/ # optional: gemeinsame TS-Typen CMS ↔ Web
|
|
||||||
├── docker-compose.yml # lokaler Prod-Parität-Stack
|
|
||||||
├── docker-compose.dev.yml # nur Postgres (wie bei FlixCooks)
|
|
||||||
├── .github/workflows/
|
|
||||||
│ ├── ci.yml
|
|
||||||
│ └── gemini-pr-review.yml # optional, aus FlixCooks übernehmen
|
|
||||||
├── .cursor/
|
|
||||||
│ └── mcp.json # MCP-Server für das Team
|
|
||||||
├── .agents/
|
|
||||||
│ ├── brain.md # Architektur für KI (Pattern aus FlixCooks)
|
|
||||||
│ └── rules/AGENT.md
|
|
||||||
└── docs/
|
|
||||||
└── COOLIFY.md
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Frontend-Stack (React + Animationen)
|
|
||||||
|
|
||||||
**Kern:**
|
|
||||||
|
|
||||||
- **Next.js App Router** – Seiten in `app/`, Server Components für CMS-Daten, Client Components nur für Animation/Interaktion
|
|
||||||
- **TypeScript** – strikt; Typen aus Payload generieren (`payload generate:types`)
|
|
||||||
- **Styling** – CSS Modules oder Tailwind (nur wenn du es willst; FlixCooks bleibt bei Vanilla CSS – für neue React-Sites ist Tailwind optional, nicht Pflicht)
|
|
||||||
|
|
||||||
**Animation-Toolkit:**
|
|
||||||
|
|
||||||
| Tool | Einsatz |
|
|
||||||
|------|---------|
|
|
||||||
| **GSAP + ScrollTrigger** | Hero-Sequences, pinned Sections, komplexe Timelines |
|
|
||||||
| **Framer Motion** | Hover, Page-Transitions, modale UI |
|
|
||||||
| **Lenis** | Smooth Scroll (wie FlixCooks: bei Overlays `lenis.stop()`) |
|
|
||||||
| **(optional) @react-three/fiber** | 3D-Hero nur wenn nötig |
|
|
||||||
|
|
||||||
**CMS-Anbindung im Web:**
|
|
||||||
|
|
||||||
- **Build-Zeit (SSG):** `generateStaticParams` + Fetch von Payload REST für Marketing-Seiten
|
|
||||||
- **On-Demand Revalidation:** Payload-Webhook → `POST /api/revalidate?secret=...` in Next.js (Inhalt ändert sich ohne Full-Redeploy)
|
|
||||||
- **Preview:** Draft-Modus mit Payload Preview-URL + Next `draftMode()`
|
|
||||||
|
|
||||||
Env im Web-Container (Coolify):
|
|
||||||
|
|
||||||
- `CMS_URL=http://payload-service:3001` (internal hostname)
|
|
||||||
- `REVALIDATE_SECRET`, `NEXT_PUBLIC_SITE_URL`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CMS-Stack (Payload auf Coolify)
|
|
||||||
|
|
||||||
**Warum Payload als Default:** Self-hosted, eine Postgres-URL, Admin unter `/admin`, Collections/Blocks für Seitenmodule, Media-Uploads, Webhooks – alles containerisierbar.
|
|
||||||
|
|
||||||
**Coolify Env (CMS):**
|
|
||||||
|
|
||||||
- `DATABASE_URL` – internal Postgres URL (gleiches Muster wie [docs/COOLIFY.md](docs/COOLIFY.md) Zeilen 33–43)
|
|
||||||
- `PAYLOAD_SECRET` – langer Zufallswert (nur Secrets, nie ins Repo)
|
|
||||||
- `NEXT_PUBLIC_SERVER_URL` – öffentliche CMS-URL (für Admin-Assets)
|
|
||||||
|
|
||||||
**Erstes Deployment:** analog FlixCooks `RUN_DB_SEED` – einmalig Migration/Seed, danach Flag entfernen.
|
|
||||||
|
|
||||||
**Sicherheit:** CMS-Admin nur über HTTPS; CORS auf Web-Domain beschränken; API-Keys für Preview/Revalidate nur als Secrets.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Docker-Images
|
|
||||||
|
|
||||||
### Web (`apps/web/Dockerfile`) – Next standalone
|
|
||||||
|
|
||||||
Mehrstufig: `node:22-alpine` → `npm ci` → `npm run build` → Runtime nur `.next/standalone` + `static` + `public`.
|
|
||||||
|
|
||||||
- `EXPOSE 3000`
|
|
||||||
- `HEALTHCHECK` auf `/api/health` (kleine Route: `{ "status": "ok" }`)
|
|
||||||
- Coolify: Port **3000**, Health Path `/api/health`
|
|
||||||
|
|
||||||
### CMS (`apps/cms/Dockerfile`)
|
|
||||||
|
|
||||||
Payload-Official-Pattern oder Node-Image mit `npm run build && npm run start`.
|
|
||||||
|
|
||||||
- `HEALTHCHECK` auf CMS-Health-Endpoint
|
|
||||||
- Volume für `/app/media` (Uploads überleben Redeploy)
|
|
||||||
|
|
||||||
### Lokales Parität-Compose
|
|
||||||
|
|
||||||
Orientierung an deinem bestehenden [docker-compose.yml](docker-compose.yml):
|
|
||||||
|
|
||||||
- `postgres` mit `healthcheck`
|
|
||||||
- `cms` `depends_on: postgres: service_healthy`
|
|
||||||
- `web` `depends_on: cms` + `DATABASE_URL` nur wenn Web eigene DB braucht (meist nicht – nur CMS nutzt DB)
|
|
||||||
|
|
||||||
Entrypoint-Pattern von [docker/entrypoint.sh](docker/entrypoint.sh) übernehmen: **DB warten → Migration → dann Prozess starten**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Coolify-Workflow (End-to-End)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Dev as Developer
|
|
||||||
participant GH as GitHub
|
|
||||||
participant GHA as GitHub Actions
|
|
||||||
participant CF as Coolify
|
|
||||||
participant Web as Next Container
|
|
||||||
participant CMS as Payload Container
|
|
||||||
|
|
||||||
Dev->>GH: push feature branch
|
|
||||||
Dev->>GH: open PR
|
|
||||||
GHA->>GHA: lint typecheck build docker
|
|
||||||
GHA-->>Dev: PR checks green
|
|
||||||
Dev->>GH: merge to main
|
|
||||||
GH->>CF: webhook deploy
|
|
||||||
CF->>CF: build CMS image
|
|
||||||
CF->>CF: build Web image
|
|
||||||
CF->>Web: rolling update
|
|
||||||
CMS->>Web: optional revalidate webhook
|
|
||||||
```
|
|
||||||
|
|
||||||
**Coolify-Konfiguration pro App:**
|
|
||||||
|
|
||||||
| Setting | Web | CMS |
|
|
||||||
|---------|-----|-----|
|
|
||||||
| Build Pack | Dockerfile | Dockerfile |
|
|
||||||
| Branch | `main` | `main` |
|
|
||||||
| Port | 3000 | 3001 |
|
|
||||||
| Health | `/api/health` | `/api/health` oder Payload-Default |
|
|
||||||
| Secrets | `REVALIDATE_SECRET`, `CMS_URL` | `DATABASE_URL`, `PAYLOAD_SECRET` |
|
|
||||||
|
|
||||||
**Checkliste Erstdeploy** (aus [docs/COOLIFY.md](docs/COOLIFY.md) übertragbar):
|
|
||||||
|
|
||||||
1. Postgres healthy, internal URL notieren
|
|
||||||
2. CMS deployen, Admin anlegen, Collections seeden
|
|
||||||
3. Web deployen mit internal `CMS_URL`
|
|
||||||
4. Domain + HTTPS (Traefik)
|
|
||||||
5. Webhook Payload → Next Revalidate testen
|
|
||||||
6. `RUN_DB_SEED` / Migration-Flags wieder aus
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GitHub Actions (CI vor Coolify)
|
|
||||||
|
|
||||||
**Workflow `ci.yml`** (bei jedem PR + push auf `main`):
|
|
||||||
|
|
||||||
1. **checkout**
|
|
||||||
2. **Node 22** + Cache (`apps/web`, `apps/cms`)
|
|
||||||
3. **Parallel jobs oder Matrix:**
|
|
||||||
- `npm run lint` (ESLint)
|
|
||||||
- `npm run typecheck` (`tsc --noEmit`)
|
|
||||||
- `npm run build` (Web + CMS)
|
|
||||||
4. **Docker build test** (ohne Push):
|
|
||||||
- `docker build -f apps/web/Dockerfile apps/web`
|
|
||||||
- `docker build -f apps/cms/Dockerfile apps/cms`
|
|
||||||
5. **(optional) Playwright** gegen `docker compose up` – Smoke: Startseite, eine CMS-Seite, Health endpoints
|
|
||||||
6. **(optional) PR Review** – bestehendes Gemini-Workflow aus FlixCooks wiederverwenden
|
|
||||||
|
|
||||||
**Branch-Schutz:** `main` nur mit grünen Required Checks mergebar.
|
|
||||||
|
|
||||||
Coolify deployt **nach** Merge – CI blockiert kaputte Images, Coolify baut das echte Production-Image (oder du pushst zu GHCR – für den Start reicht Coolify-eigener Build).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## KI-gestützte Entwicklung mit MCP
|
|
||||||
|
|
||||||
**Cursor `mcp.json` (Team-Standard):**
|
|
||||||
|
|
||||||
| MCP | Zweck |
|
|
||||||
|-----|--------|
|
|
||||||
| **GitHub** | Issues, PRs, Actions-Logs aus dem Chat |
|
|
||||||
| **Context7** (oder Fetch) | Aktuelle Next.js / Payload / GSAP-Docs |
|
|
||||||
| **Postgres** (optional, nur Dev) | Content/Debugging – nie Production-Credentials im Repo |
|
|
||||||
| **Filesystem** | Standard in Cursor |
|
|
||||||
|
|
||||||
**Projekt-Wissen für Agenten** (aus FlixCooks übernehmen):
|
|
||||||
|
|
||||||
- [.agents/brain.md](.agents/brain.md) – Architektur, Env-Regeln, Coolify-Hosts
|
|
||||||
- [.agents/rules/AGENT.md](.agents/rules/AGENT.md) – Commit/PR/TODO-Konventionen
|
|
||||||
- `docs/COOLIFY.md` – Deploy-Runbook pro Projekt
|
|
||||||
|
|
||||||
**Typischer KI-Workflow:**
|
|
||||||
|
|
||||||
1. Ticket/Issue in GitHub (MCP)
|
|
||||||
2. Feature-Branch; Agent ändert `apps/web` + Payload-Collection
|
|
||||||
3. `docker compose up` lokal; Agent nutzt Health-URLs
|
|
||||||
4. PR → CI grün → Gemini-Review optional
|
|
||||||
5. Merge → Coolify
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Entwickler-Alltag (Kurzablauf)
|
|
||||||
|
|
||||||
1. `cp .env.example .env` – lokale URLs
|
|
||||||
2. `docker compose -f docker-compose.dev.yml up -d` (nur Postgres) **oder** volles `docker compose up`
|
|
||||||
3. `npm run dev` in `apps/web` und `apps/cms` (schneller Hot Reload) **oder** alles in Containern
|
|
||||||
4. In Payload Inhalte pflegen → Webhook triggert Revalidate
|
|
||||||
5. `git push` → PR → CI → merge → Coolify rebuild
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bezug zu FlixCooks (dieses Repo)
|
|
||||||
|
|
||||||
FlixCooks ist heute **Vanilla PHP + Postgres + eingebautes `admin.php`-CMS** – kein React. Das ist ein **paralleler Stack**, kein Widerspruch:
|
|
||||||
|
|
||||||
| Aspekt | FlixCooks (aktuell) | Neuer React-Stack |
|
|
||||||
|--------|---------------------|-------------------|
|
|
||||||
| Frontend | PHP-Templates | Next.js + React |
|
|
||||||
| CMS | `admin.php` | Payload (Container) |
|
|
||||||
| DB | Postgres | Postgres |
|
|
||||||
| Deploy | [Dockerfile](Dockerfile) + [COOLIFY.md](docs/COOLIFY.md) | gleiches Muster, zwei App-Services |
|
|
||||||
| Animationen | Lenis, Vanilla CSS | Lenis + GSAP + Framer |
|
|
||||||
|
|
||||||
Du kannst FlixCooks auf Coolify weiterbetreiben und **neue Projekte** im Monorepo-Template starten. Eine spätere Migration FlixCooks → Next wäre ein separates Projekt (Content-Export aus Postgres/JSON → Payload-Collections).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Nächste konkrete Schritte (nach Plan-Freigabe)
|
|
||||||
|
|
||||||
1. **Greenfield-Repo** aus der Struktur oben scaffolden (oder `create-payload-app` + Next in Monorepo).
|
|
||||||
2. **Minimale Collections** in Payload: `pages`, `recipes` (oder `projects`), `siteSettings`, Media.
|
|
||||||
3. **Dockerfiles + compose** + `docs/COOLIFY.md` vom FlixCooks-Muster kopieren/adaptieren.
|
|
||||||
4. **GitHub Actions `ci.yml`** anlegen.
|
|
||||||
5. **Coolify:** Postgres → CMS → Web → Webhook Revalidate testen.
|
|
||||||
6. **`.cursor/mcp.json` + `.agents/brain.md`** für das neue Repo.
|
|
||||||
|
|
||||||
Wenn du willst, kann im nächsten Schritt ein **konkretes Starter-Repo** (Dateien + minimale Hero-Animation + eine Payload-Collection) direkt in einem neuen Ordner oder Branch angelegt werden.
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
name: "Gemini AI PR Review"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
review:
|
|
||||||
name: "Code Review"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Gemini AI Review
|
|
||||||
uses: petarzarkov/gemini-code-review-action@v1.0.4 # Pinned version for stability
|
|
||||||
env:
|
|
||||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
model: gemini-2.5-flash-lite # Cheapest Gemini model - $0.10/1M input, $0.40/1M output
|
|
||||||
# You can add custom prompt instructions here if needed:
|
|
||||||
# prompt: "You are a senior software engineer. Review this PR..."
|
|
||||||
@@ -196,6 +196,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') {
|
||||||
|
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['token'] ?? '')) {
|
||||||
|
$errors[] = 'Invalid form token, please retry.';
|
||||||
|
} else {
|
||||||
$slugDel = trim($_POST['slug'] ?? '');
|
$slugDel = trim($_POST['slug'] ?? '');
|
||||||
if ($slugDel !== '' && delete_recipe($slugDel)) {
|
if ($slugDel !== '' && delete_recipe($slugDel)) {
|
||||||
$message = 'Recipe deleted.';
|
$message = 'Recipe deleted.';
|
||||||
@@ -205,7 +208,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
|||||||
}
|
}
|
||||||
$editing = null;
|
$editing = null;
|
||||||
$editIndex = null;
|
$editIndex = null;
|
||||||
|
}
|
||||||
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save') {
|
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save') {
|
||||||
|
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['token'] ?? '')) {
|
||||||
|
$errors[] = 'Invalid form token, please retry.';
|
||||||
|
} else {
|
||||||
// helpers
|
// helpers
|
||||||
$parse_csv = fn($text) => array_values(array_filter(array_map('trim', explode(',', $text ?? '')), 'strlen'));
|
$parse_csv = fn($text) => array_values(array_filter(array_map('trim', explode(',', $text ?? '')), 'strlen'));
|
||||||
$parse_lines = fn($text) => array_values(array_filter(array_map('trim', preg_split('/\\r?\\n/', $text ?? '')), 'strlen'));
|
$parse_lines = fn($text) => array_values(array_filter(array_map('trim', preg_split('/\\r?\\n/', $text ?? '')), 'strlen'));
|
||||||
@@ -344,6 +351,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
|||||||
$message = 'Could not save to database.';
|
$message = 'Could not save to database.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} // end CSRF else
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -527,6 +535,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
|||||||
<a class="button" href="/admin.php?edit=<?php echo urlencode($r['slug']); ?>" style="padding:8px 12px;">Edit</a>
|
<a class="button" href="/admin.php?edit=<?php echo urlencode($r['slug']); ?>" style="padding:8px 12px;">Edit</a>
|
||||||
<form method="post" style="margin:0;" onsubmit="return confirm('Delete this recipe?');">
|
<form method="post" style="margin:0;" onsubmit="return confirm('Delete this recipe?');">
|
||||||
<input type="hidden" name="action" value="delete">
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="token" value="<?php echo e($_SESSION['csrf_token']); ?>">
|
||||||
<input type="hidden" name="slug" value="<?php echo e($r['slug']); ?>">
|
<input type="hidden" name="slug" value="<?php echo e($r['slug']); ?>">
|
||||||
<button type="submit" class="button" style="padding:8px 12px;">Delete</button>
|
<button type="submit" class="button" style="padding:8px 12px;">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -538,6 +547,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delet
|
|||||||
|
|
||||||
<form class="admin-form" method="post">
|
<form class="admin-form" method="post">
|
||||||
<input type="hidden" name="action" value="save">
|
<input type="hidden" name="action" value="save">
|
||||||
|
<input type="hidden" name="token" value="<?php echo e($_SESSION['csrf_token']); ?>">
|
||||||
<?php if ($editing): ?>
|
<?php if ($editing): ?>
|
||||||
<input type="hidden" name="slug_original" value="<?php echo e($editing['slug']); ?>">
|
<input type="hidden" name="slug_original" value="<?php echo e($editing['slug']); ?>">
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ services:
|
|||||||
- "8080:80"
|
- "8080:80"
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://flixcooks:flixcooks_prod@postgres:5432/flixcooks
|
DATABASE_URL: postgresql://flixcooks:flixcooks_prod@postgres:5432/flixcooks
|
||||||
FLIXCOOKS_ADMIN_KEY: ${FLIXCOOKS_ADMIN_KEY:-change-me-in-production}
|
FLIXCOOKS_ADMIN_KEY: ${FLIXCOOKS_ADMIN_KEY:-}
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
Reference in New Issue
Block a user