Inicial commit
This commit is contained in:
commit
e1f55c8411
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"WebFetch(domain:portless.sh)",
|
||||||
|
"WebFetch(domain:github.com)",
|
||||||
|
"Bash(pnpm -v)",
|
||||||
|
"Bash(npm -v)",
|
||||||
|
"Bash(docker -v)",
|
||||||
|
"Bash(docker compose *)",
|
||||||
|
"Bash(pnpm config *)",
|
||||||
|
"Bash(pnpm install *)",
|
||||||
|
"Bash(echo \"EXIT=$?\")",
|
||||||
|
"Bash(pnpm --filter cms generate:importmap)",
|
||||||
|
"Bash(pnpm --filter cms generate:types)",
|
||||||
|
"Bash(pnpm --filter web typecheck)",
|
||||||
|
"Bash(pnpm --filter web build)",
|
||||||
|
"Bash(pnpm --filter cms build)",
|
||||||
|
"Bash(PORT=3000 pnpm --filter cms dev)",
|
||||||
|
"Bash(curl -s --retry-connrefused --retry 40 --retry-delay 2 -o /dev/null -w \"connect ok, http=%{http_code}\\\\n\" \"http://localhost:3000/api/posts\")",
|
||||||
|
"Bash(curl -s \"http://localhost:3000/api/posts\")",
|
||||||
|
"Bash(docker exec *)",
|
||||||
|
"Bash(python3 -c \"import sys,json;d=json.load\\(sys.stdin\\);print\\('totalDocs',d['totalDocs']\\);[print\\('-',p['title'],'/',p['slug']\\) for p in d['docs']]\")",
|
||||||
|
"Bash(PORT=4321 pnpm --filter web dev:plain)",
|
||||||
|
"Bash(echo \"started web dev \\(pid $!\\)\")",
|
||||||
|
"Bash(curl -s --retry-connrefused --retry 30 --retry-delay 1 -o /dev/null -w \"home http=%{http_code}\\\\n\" \"http://localhost:4321/\")",
|
||||||
|
"Bash(curl -s \"http://localhost:4321/\")",
|
||||||
|
"Bash(curl -s \"http://localhost:4321/posts/hello-from-payload\")",
|
||||||
|
"Bash(pkill -f \"next dev\")",
|
||||||
|
"Bash(pkill -f \"astro dev\")",
|
||||||
|
"Bash(pkill -f \"next-server\")",
|
||||||
|
"Bash(pnpm exec *)",
|
||||||
|
"Bash(python3 -c \"import sys,json;d=json.load\\(sys.stdin\\);[print\\('-',t['taskId']\\) for t in d['tasks']]\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
############################################################
|
||||||
|
# Single source of truth for the whole stack.
|
||||||
|
# cp .env.example .env
|
||||||
|
#
|
||||||
|
# Used by BOTH docker-compose.yml (Supabase backend) and the
|
||||||
|
# apps (Payload CMS + Astro).
|
||||||
|
#
|
||||||
|
# ⚠️ These are INSECURE DEV DEFAULTS. Regenerate every secret
|
||||||
|
# before deploying anywhere real. See README "Production".
|
||||||
|
############################################################
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Postgres (Supabase DB)
|
||||||
|
########################
|
||||||
|
POSTGRES_HOST=db
|
||||||
|
POSTGRES_DB=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
|
|
||||||
|
########################
|
||||||
|
# JWT / API keys
|
||||||
|
# The ANON_KEY and SERVICE_ROLE_KEY below are signed with this
|
||||||
|
# exact JWT_SECRET. If you change JWT_SECRET you MUST regenerate
|
||||||
|
# both keys (see README) or Auth/Kong will reject them.
|
||||||
|
########################
|
||||||
|
JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||||
|
JWT_EXPIRY=3600
|
||||||
|
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||||
|
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
|
||||||
|
|
||||||
|
# New-style opaque API keys (optional; leave blank to disable)
|
||||||
|
SUPABASE_PUBLISHABLE_KEY=
|
||||||
|
SUPABASE_SECRET_KEY=
|
||||||
|
ANON_KEY_ASYMMETRIC=
|
||||||
|
SERVICE_ROLE_KEY_ASYMMETRIC=
|
||||||
|
|
||||||
|
########################
|
||||||
|
# API gateway (Kong) + public URLs
|
||||||
|
########################
|
||||||
|
KONG_HTTP_PORT=8000
|
||||||
|
KONG_HTTPS_PORT=8443
|
||||||
|
SUPABASE_PUBLIC_URL=http://localhost:8000
|
||||||
|
API_EXTERNAL_URL=http://localhost:8000/auth/v1
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Studio dashboard
|
||||||
|
########################
|
||||||
|
DASHBOARD_USERNAME=supabase
|
||||||
|
DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated
|
||||||
|
STUDIO_DEFAULT_ORGANIZATION=Default Organization
|
||||||
|
STUDIO_DEFAULT_PROJECT=cdrdpyj
|
||||||
|
PG_META_CRYPTO_KEY=your-encryption-key-32-chars-min
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Auth (GoTrue)
|
||||||
|
########################
|
||||||
|
SITE_URL=https://web.localhost
|
||||||
|
ADDITIONAL_REDIRECT_URLS=
|
||||||
|
DISABLE_SIGNUP=false
|
||||||
|
ENABLE_EMAIL_SIGNUP=true
|
||||||
|
ENABLE_EMAIL_AUTOCONFIRM=true
|
||||||
|
ENABLE_ANONYMOUS_USERS=false
|
||||||
|
ENABLE_PHONE_SIGNUP=false
|
||||||
|
ENABLE_PHONE_AUTOCONFIRM=false
|
||||||
|
MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify"
|
||||||
|
MAILER_URLPATHS_INVITE="/auth/v1/verify"
|
||||||
|
MAILER_URLPATHS_RECOVERY="/auth/v1/verify"
|
||||||
|
MAILER_URLPATHS_EMAIL_CHANGE="/auth/v1/verify"
|
||||||
|
SMTP_ADMIN_EMAIL=admin@example.com
|
||||||
|
SMTP_HOST=supabase-mail
|
||||||
|
SMTP_PORT=2500
|
||||||
|
SMTP_USER=fake_mail_user
|
||||||
|
SMTP_PASS=fake_mail_password
|
||||||
|
SMTP_SENDER_NAME=fake_sender
|
||||||
|
|
||||||
|
########################
|
||||||
|
# REST (PostgREST)
|
||||||
|
########################
|
||||||
|
PGRST_DB_SCHEMAS=public,graphql_public
|
||||||
|
PGRST_DB_MAX_ROWS=1000
|
||||||
|
PGRST_DB_EXTRA_SEARCH_PATH=public
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Storage + imgproxy
|
||||||
|
########################
|
||||||
|
GLOBAL_S3_BUCKET=stub
|
||||||
|
REGION=stub
|
||||||
|
STORAGE_TENANT_ID=stub
|
||||||
|
S3_PROTOCOL_ACCESS_KEY_ID=625729a08b95bf1b7ff351a663f3a23c
|
||||||
|
S3_PROTOCOL_ACCESS_KEY_SECRET=850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907
|
||||||
|
IMGPROXY_AUTO_WEBP=true
|
||||||
|
|
||||||
|
############################################################
|
||||||
|
# Applications
|
||||||
|
############################################################
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Payload CMS (apps/cms)
|
||||||
|
# Runs on plain http://localhost:3000 in host dev so Astro's
|
||||||
|
# server-side fetch stays plain HTTP (portless HTTPS certs are
|
||||||
|
# not trusted by Node). DATABASE_URI connects via the published
|
||||||
|
# Postgres port; the compose `apps` profile overrides host->db.
|
||||||
|
########################
|
||||||
|
DATABASE_URI=postgresql://postgres:postgres@localhost:5432/postgres
|
||||||
|
PAYLOAD_SECRET=dev-only-change-me-6f1c2b8a9e4d
|
||||||
|
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Astro (apps/web) — runs behind portless at https://web.localhost
|
||||||
|
# Where the frontend reads content from Payload's REST API.
|
||||||
|
# Vars prefixed PUBLIC_ are exposed to the browser by Astro.
|
||||||
|
########################
|
||||||
|
PUBLIC_PAYLOAD_API_URL=http://localhost:3000/api
|
||||||
|
PUBLIC_SUPABASE_URL=http://localhost:8000
|
||||||
|
PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Turborepo
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# Builds
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Supabase persisted data & generated content
|
||||||
|
supabase/volumes/db/data
|
||||||
|
supabase/volumes/storage/*
|
||||||
|
!supabase/volumes/storage/.gitkeep
|
||||||
|
|
||||||
|
# Payload generated
|
||||||
|
apps/cms/media
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
auto-install-peers=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
# cdrdpyj
|
||||||
|
|
||||||
|
A TypeScript **Turborepo** wiring an **Astro** frontend to a **Payload CMS**
|
||||||
|
backend, both running against a **self-hosted Supabase** stack (Postgres),
|
||||||
|
orchestrated from a single **docker-compose** file — with **[portless]** for
|
||||||
|
stable `*.localhost` dev URLs.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ REST /api ┌──────────────┐ SQL ┌───────────────────────────┐
|
||||||
|
│ Astro (web) │ ──────────────────────▶ │ Payload (cms)│ ─────────────────▶ │ Supabase (Postgres 17) │
|
||||||
|
│ SSR, :4321 │ │ Next, :3000 │ schema: payload │ db · studio · kong · │
|
||||||
|
│ web.localhost│ │ │ │ auth · rest · storage │
|
||||||
|
└──────────────┘ └──────────────┘ └───────────────────────────┘
|
||||||
|
▲ portless Studio dashboard :8000
|
||||||
|
```
|
||||||
|
|
||||||
|
- **apps/web** — Astro 5 (SSR via `@astrojs/node`). Reads posts from Payload's REST API.
|
||||||
|
- **apps/cms** — Payload 3 (on Next.js 16). Stores content in a dedicated `payload` schema in Postgres.
|
||||||
|
- **Supabase** — trimmed self-hosted stack: `db`, `studio`, `meta`, `kong`, `auth`, `rest`, `storage`, `imgproxy`.
|
||||||
|
- **packages/typescript-config** — shared `tsconfig` bases.
|
||||||
|
|
||||||
|
[portless]: https://portless.sh
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
| Tool | Version | Notes |
|
||||||
|
| ------- | ------- | ----- |
|
||||||
|
| Node | ≥ 24 | portless and Payload both require Node 24+ |
|
||||||
|
| pnpm | ≥ 9 | `corepack enable` or `npm i -g pnpm` |
|
||||||
|
| Docker | + Compose v2 | for the Supabase backend |
|
||||||
|
| portless| latest | `pnpm add -g portless` (optional but recommended) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Configure — one env file drives the whole stack
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# 2. Install workspace deps
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# 3. Start the Supabase backend (Postgres + Studio + Auth + REST + Storage)
|
||||||
|
docker compose up -d # first run pulls images; give it a minute
|
||||||
|
|
||||||
|
# 4. Run the apps (Astro + Payload) with hot reload
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open:
|
||||||
|
|
||||||
|
| What | URL |
|
||||||
|
| --------------- | ------------------------------------- |
|
||||||
|
| Astro frontend | http://localhost:4321 (or the portless URL below) |
|
||||||
|
| Payload admin | http://localhost:3000/admin |
|
||||||
|
| Supabase Studio | http://localhost:8000 (login: `supabase` / `this_password_is_insecure_and_should_be_updated`) |
|
||||||
|
| Postgres | `postgresql://postgres:postgres@localhost:5432/postgres` |
|
||||||
|
|
||||||
|
First time in Payload admin you'll create the initial admin user. Then add a
|
||||||
|
**Post** (give it a `slug`) and it appears on the Astro homepage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## portless — stable `*.localhost` URLs
|
||||||
|
|
||||||
|
[portless] gives the frontend a memorable HTTPS URL instead of a port number,
|
||||||
|
and injects `$PORT` automatically.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add -g portless # once
|
||||||
|
pnpm dev # Astro is started via `portless web astro dev`
|
||||||
|
```
|
||||||
|
|
||||||
|
The web app is served at **https://web.localhost**. On first run portless
|
||||||
|
generates and trusts a local CA and binds to `:443` (may prompt for sudo).
|
||||||
|
|
||||||
|
> **Why only the frontend?** Payload stays on plain `http://localhost:3000` so
|
||||||
|
> Astro's server-side `fetch` to it uses plain HTTP — Node doesn't trust
|
||||||
|
> portless's local CA for server-to-server HTTPS. To also route Payload through
|
||||||
|
> portless (https://cms.localhost), run `pnpm --filter cms dev:portless` and set
|
||||||
|
> `NODE_EXTRA_CA_CERTS` to portless's CA so Astro can reach it over HTTPS.
|
||||||
|
|
||||||
|
Prefer no portless? Use `pnpm --filter web dev:plain` (plain `astro dev`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Everything in Docker (optional)
|
||||||
|
|
||||||
|
The apps also ship with Dockerfiles behind a compose `apps` profile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile apps up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs Astro (`:4321`) and Payload (`:3000`) as containers alongside
|
||||||
|
Supabase. `DATABASE_URI` and the Astro→Payload URL are rewired to the internal
|
||||||
|
Docker network automatically. The recommended day-to-day loop is still
|
||||||
|
`docker compose up -d` (backend) + `pnpm dev` (apps) for hot reload.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
Run from the repo root:
|
||||||
|
|
||||||
|
| Command | Does |
|
||||||
|
| ------------------------ | ---- |
|
||||||
|
| `pnpm dev` | Turbo runs both apps (Astro via portless, Payload plain) |
|
||||||
|
| `pnpm build` | Build all apps |
|
||||||
|
| `pnpm typecheck` | Type-check all apps |
|
||||||
|
| `pnpm supabase:up` / `:down` | Start / stop the Supabase backend |
|
||||||
|
| `pnpm supabase:logs` | Tail backend logs |
|
||||||
|
| `pnpm supabase:reset` | ⚠️ Stop and **delete** all Postgres data + volumes |
|
||||||
|
| `pnpm stack:up` / `:down`| Backend **and** apps in Docker (`apps` profile) |
|
||||||
|
|
||||||
|
Per-app: `pnpm --filter cms generate:types`, `pnpm --filter cms generate:importmap`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How configuration works
|
||||||
|
|
||||||
|
There is **one** `.env` at the repo root, shared by everything:
|
||||||
|
|
||||||
|
- **docker-compose** reads it natively.
|
||||||
|
- **Payload** loads it from the root in `apps/cms/src/payload.config.ts`.
|
||||||
|
- **Astro** loads it via `vite.envDir` pointed at the root; `PUBLIC_*` vars are
|
||||||
|
exposed to the browser.
|
||||||
|
|
||||||
|
The Supabase `ANON_KEY` / `SERVICE_ROLE_KEY` in `.env.example` are the standard
|
||||||
|
Supabase demo keys, signed with the matching demo `JWT_SECRET`. They work out of
|
||||||
|
the box for local dev. **Changing `JWT_SECRET` invalidates them** — regenerate
|
||||||
|
with Supabase's `generate-keys` steps if you do.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── docker-compose.yml # single-file Supabase stack (+ apps profile)
|
||||||
|
├── .env.example # one source of truth for all config
|
||||||
|
├── turbo.json # task pipeline
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Astro SSR frontend
|
||||||
|
│ └── cms/ # Payload 3 (Next.js) CMS
|
||||||
|
├── packages/
|
||||||
|
│ └── typescript-config/ # shared tsconfig bases
|
||||||
|
└── supabase/
|
||||||
|
└── volumes/ # kong config + Postgres init SQL (roles, jwt, payload schema)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production notes
|
||||||
|
|
||||||
|
This scaffold is tuned for local development. Before deploying anywhere real:
|
||||||
|
|
||||||
|
- Regenerate **every** secret in `.env` (`POSTGRES_PASSWORD`, `JWT_SECRET` +
|
||||||
|
matching keys, `PAYLOAD_SECRET`, `DASHBOARD_PASSWORD`, `PG_META_CRYPTO_KEY`,
|
||||||
|
the S3 protocol keys).
|
||||||
|
- Put Kong / the apps behind TLS and real domains; set `SITE_URL`,
|
||||||
|
`API_EXTERNAL_URL`, `SUPABASE_PUBLIC_URL`, `PAYLOAD_PUBLIC_SERVER_URL`,
|
||||||
|
`PUBLIC_PAYLOAD_API_URL` accordingly.
|
||||||
|
- Switch Payload from dev "push" to committed migrations
|
||||||
|
(`pnpm --filter cms payload migrate:create`).
|
||||||
|
- Consider adding back the Supabase services this scaffold trims (Realtime,
|
||||||
|
Edge Functions, Supavisor pooler) if you need them — see
|
||||||
|
[Supabase self-hosting docs](https://supabase.com/docs/guides/self-hosting/docker).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Astro homepage shows "Couldn't reach the CMS"** — Payload isn't running or
|
||||||
|
the DB is down. Ensure `docker compose up -d` is healthy and `pnpm dev` shows
|
||||||
|
the `cms` server on `:3000`.
|
||||||
|
- **Payload can't connect to Postgres** — the DB exposes `:5432`; confirm
|
||||||
|
`docker compose ps` shows `cdrdpyj-db` healthy and `DATABASE_URI` matches.
|
||||||
|
- **Ports already in use** — `5432`, `8000`, `3000`, `4321` must be free (or
|
||||||
|
change them in `.env` / the app dev scripts).
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Payload CMS (apps/cms) — built from the monorepo root context.
|
||||||
|
# docker compose --profile apps build cms
|
||||||
|
FROM node:24-slim AS base
|
||||||
|
ENV PNPM_HOME=/pnpm
|
||||||
|
ENV PATH=$PNPM_HOME:$PATH
|
||||||
|
RUN corepack enable
|
||||||
|
# sharp / libvips runtime deps
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
openssl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /repo
|
||||||
|
|
||||||
|
# ---- Install dependencies (whole workspace, respecting the lockfile) ----
|
||||||
|
FROM base AS deps
|
||||||
|
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml* .npmrc* ./
|
||||||
|
COPY packages/typescript-config/package.json packages/typescript-config/
|
||||||
|
COPY apps/cms/package.json apps/cms/
|
||||||
|
COPY apps/web/package.json apps/web/
|
||||||
|
RUN pnpm install --frozen-lockfile || pnpm install
|
||||||
|
|
||||||
|
# ---- Build the CMS ----
|
||||||
|
FROM base AS build
|
||||||
|
COPY --from=deps /repo/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm --filter cms build
|
||||||
|
|
||||||
|
# ---- Runtime ----
|
||||||
|
FROM base AS runtime
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY --from=build /repo ./
|
||||||
|
WORKDIR /repo/apps/cms
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["pnpm", "start"]
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { dirname } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = dirname(__filename)
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
})
|
||||||
|
|
||||||
|
const eslintConfig = [
|
||||||
|
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||||
|
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
vars: 'all',
|
||||||
|
args: 'after-used',
|
||||||
|
ignoreRestSiblings: false,
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
destructuredArrayIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^(_|ignore)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ['.next/', 'src/payload-types.ts', 'src/payload-generated-schema.ts'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default eslintConfig
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { withPayload } from '@payloadcms/next/withPayload'
|
||||||
|
import type { NextConfig } from 'next'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
images: {
|
||||||
|
localPatterns: [
|
||||||
|
{
|
||||||
|
pathname: '/api/media/file/**',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
webpack: (webpackConfig) => {
|
||||||
|
webpackConfig.resolve.extensionAlias = {
|
||||||
|
'.cjs': ['.cts', '.cjs'],
|
||||||
|
'.js': ['.ts', '.tsx', '.js', '.jsx'],
|
||||||
|
'.mjs': ['.mts', '.mjs'],
|
||||||
|
}
|
||||||
|
|
||||||
|
return webpackConfig
|
||||||
|
},
|
||||||
|
turbopack: {
|
||||||
|
// Monorepo root so Turbopack resolves pnpm-hoisted packages correctly.
|
||||||
|
root: path.resolve(dirname, '../../'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withPayload(nextConfig, { devBundleServerPackages: false })
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"name": "cms",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||||
|
"dev:portless": "cross-env NODE_OPTIONS=--no-deprecation portless cms next dev",
|
||||||
|
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||||
|
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
||||||
|
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"lint": "cross-env NODE_OPTIONS=--no-deprecation eslint .",
|
||||||
|
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||||
|
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||||
|
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||||
|
"clean": "rm -rf .next node_modules"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@payloadcms/db-postgres": "3.87.0",
|
||||||
|
"@payloadcms/next": "3.87.0",
|
||||||
|
"@payloadcms/richtext-lexical": "3.87.0",
|
||||||
|
"@payloadcms/ui": "3.87.0",
|
||||||
|
"cross-env": "10.1.0",
|
||||||
|
"dotenv": "16.4.7",
|
||||||
|
"graphql": "^16.8.1",
|
||||||
|
"next": "16.2.7",
|
||||||
|
"payload": "3.87.0",
|
||||||
|
"react": "19.2.6",
|
||||||
|
"react-dom": "19.2.6",
|
||||||
|
"sharp": "0.34.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/typescript-config": "workspace:*",
|
||||||
|
"@types/node": "^24.12.3",
|
||||||
|
"@types/react": "19.2.14",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"eslint": "^9.16.0",
|
||||||
|
"eslint-config-next": "16.2.7",
|
||||||
|
"portless": "^0.15.5",
|
||||||
|
"sass": "^1.83.0",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.15.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react'
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
description: 'A blank template using Payload in a Next.js app.',
|
||||||
|
title: 'Payload Blank Template',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||||
|
const { children } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
<main>{children}</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { headers as getHeaders } from 'next/headers.js'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import React from 'react'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import config from '@/payload.config'
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
export default async function HomePage() {
|
||||||
|
const headers = await getHeaders()
|
||||||
|
const payloadConfig = await config
|
||||||
|
const payload = await getPayload({ config: payloadConfig })
|
||||||
|
const { user } = await payload.auth({ headers })
|
||||||
|
|
||||||
|
const fileURL = `vscode://file/${fileURLToPath(import.meta.url)}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="home">
|
||||||
|
<div className="content">
|
||||||
|
<picture>
|
||||||
|
<source srcSet="https://raw.githubusercontent.com/payloadcms/payload/3.x/packages/ui/src/assets/payload-favicon.svg" />
|
||||||
|
<Image
|
||||||
|
alt="Payload Logo"
|
||||||
|
height={65}
|
||||||
|
src="https://raw.githubusercontent.com/payloadcms/payload/3.x/packages/ui/src/assets/payload-favicon.svg"
|
||||||
|
width={65}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
{!user && <h1>Welcome to your new project.</h1>}
|
||||||
|
{user && <h1>Welcome back, {user.email}</h1>}
|
||||||
|
<div className="links">
|
||||||
|
<a
|
||||||
|
className="admin"
|
||||||
|
href={payloadConfig.routes.admin}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Go to admin panel
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="docs"
|
||||||
|
href="https://payloadcms.com/docs"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="footer">
|
||||||
|
<p>Update this page by editing</p>
|
||||||
|
<a className="codeLink" href={fileURL}>
|
||||||
|
<code>app/(frontend)/page.tsx</code>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
:root {
|
||||||
|
--font-mono: 'Roboto Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 32px;
|
||||||
|
|
||||||
|
background: rgb(0, 0, 0);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: system-ui;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 32px;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
color: rgb(1000, 1000, 1000);
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 40px 0;
|
||||||
|
font-size: 64px;
|
||||||
|
line-height: 70px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
margin: 24px 0;
|
||||||
|
font-size: 42px;
|
||||||
|
line-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
font-size: 38px;
|
||||||
|
line-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 24px 0;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
margin: calc(var(--base) * 0.75) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: currentColor;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
opacity: 0.8;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 45px;
|
||||||
|
max-width: 1024px;
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin {
|
||||||
|
color: rgb(0, 0, 0);
|
||||||
|
background: rgb(1000, 1000, 1000);
|
||||||
|
border: 1px solid rgb(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs {
|
||||||
|
color: rgb(1000, 1000, 1000);
|
||||||
|
background: rgb(0, 0, 0);
|
||||||
|
border: 1px solid rgb(1000, 1000, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeLink {
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
background: rgb(60, 60, 60);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||||
|
import { importMap } from '../importMap'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
segments: string[]
|
||||||
|
}>
|
||||||
|
searchParams: Promise<{
|
||||||
|
[key: string]: string | string[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||||
|
generatePageMetadata({ config, params, searchParams })
|
||||||
|
|
||||||
|
const NotFound = ({ params, searchParams }: Args) =>
|
||||||
|
NotFoundPage({ config, params, searchParams, importMap })
|
||||||
|
|
||||||
|
export default NotFound
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||||
|
import { importMap } from '../importMap'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
segments: string[]
|
||||||
|
}>
|
||||||
|
searchParams: Promise<{
|
||||||
|
[key: string]: string | string[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||||
|
generatePageMetadata({ config, params, searchParams })
|
||||||
|
|
||||||
|
const Page = ({ params, searchParams }: Args) =>
|
||||||
|
RootPage({ config, params, searchParams, importMap })
|
||||||
|
|
||||||
|
export default Page
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||||
|
|
||||||
|
/** @type import('payload').ImportMap */
|
||||||
|
export const importMap = {
|
||||||
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import '@payloadcms/next/css'
|
||||||
|
import {
|
||||||
|
REST_DELETE,
|
||||||
|
REST_GET,
|
||||||
|
REST_OPTIONS,
|
||||||
|
REST_PATCH,
|
||||||
|
REST_POST,
|
||||||
|
REST_PUT,
|
||||||
|
} from '@payloadcms/next/routes'
|
||||||
|
|
||||||
|
export const GET = REST_GET(config)
|
||||||
|
export const POST = REST_POST(config)
|
||||||
|
export const DELETE = REST_DELETE(config)
|
||||||
|
export const PATCH = REST_PATCH(config)
|
||||||
|
export const PUT = REST_PUT(config)
|
||||||
|
export const OPTIONS = REST_OPTIONS(config)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import '@payloadcms/next/css'
|
||||||
|
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||||
|
|
||||||
|
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||||
|
|
||||||
|
export const POST = GRAPHQL_POST(config)
|
||||||
|
|
||||||
|
export const OPTIONS = REST_OPTIONS(config)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// Add your custom admin panel styles here.
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import '@payloadcms/next/css'
|
||||||
|
import type { ServerFunctionClient } from 'payload'
|
||||||
|
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { importMap } from './admin/importMap.js'
|
||||||
|
import './custom.scss'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverFunction: ServerFunctionClient = async function (args) {
|
||||||
|
'use server'
|
||||||
|
return handleServerFunctions({
|
||||||
|
...args,
|
||||||
|
config,
|
||||||
|
importMap,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout = ({ children }: Args) => (
|
||||||
|
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||||
|
{children}
|
||||||
|
</RootLayout>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Layout
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
export const Media: CollectionConfig = {
|
||||||
|
slug: 'media',
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'alt',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
upload: true,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
export const Posts: CollectionConfig = {
|
||||||
|
slug: 'posts',
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'title',
|
||||||
|
defaultColumns: ['title', 'slug', 'publishedAt', 'updatedAt'],
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
// Public read so the Astro frontend can fetch content via the REST API.
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'slug',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
admin: {
|
||||||
|
description: 'URL-friendly identifier used by the frontend.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'excerpt',
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'richText',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'coverImage',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'publishedAt',
|
||||||
|
type: 'date',
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
date: { pickerAppearance: 'dayAndTime' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
export const Users: CollectionConfig = {
|
||||||
|
slug: 'users',
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
auth: true,
|
||||||
|
fields: [
|
||||||
|
// Email added by default
|
||||||
|
// Add more fields as needed
|
||||||
|
],
|
||||||
|
versions: false,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,391 @@
|
||||||
|
/* 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;
|
||||||
|
media: Media;
|
||||||
|
posts: Post;
|
||||||
|
'payload-kv': PayloadKv;
|
||||||
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
|
'payload-preferences': PayloadPreference;
|
||||||
|
'payload-migrations': PayloadMigration;
|
||||||
|
};
|
||||||
|
collectionsJoins: {};
|
||||||
|
collectionsSelect: {
|
||||||
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
|
media: MediaSelect<false> | MediaSelect<true>;
|
||||||
|
posts: PostsSelect<false> | PostsSelect<true>;
|
||||||
|
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||||
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
|
};
|
||||||
|
db: {
|
||||||
|
defaultIDType: number;
|
||||||
|
};
|
||||||
|
fallbackLocale: null;
|
||||||
|
globals: {};
|
||||||
|
globalsSelect: {};
|
||||||
|
locale: null;
|
||||||
|
widgets: {
|
||||||
|
collections: CollectionsWidget;
|
||||||
|
};
|
||||||
|
user: User;
|
||||||
|
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: number;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
email: string;
|
||||||
|
resetPasswordToken?: string | null;
|
||||||
|
resetPasswordExpiration?: string | null;
|
||||||
|
salt?: string | null;
|
||||||
|
hash?: string | null;
|
||||||
|
loginAttempts?: number | null;
|
||||||
|
lockUntil?: string | null;
|
||||||
|
sessions?:
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
createdAt?: string | null;
|
||||||
|
expiresAt: string;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
password?: string | null;
|
||||||
|
collection: 'users';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "media".
|
||||||
|
*/
|
||||||
|
export interface Media {
|
||||||
|
id: number;
|
||||||
|
alt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
url?: string | null;
|
||||||
|
thumbnailURL?: string | null;
|
||||||
|
filename?: string | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
filesize?: number | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
focalX?: number | null;
|
||||||
|
focalY?: number | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "posts".
|
||||||
|
*/
|
||||||
|
export interface Post {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* URL-friendly identifier used by the frontend.
|
||||||
|
*/
|
||||||
|
slug: string;
|
||||||
|
excerpt?: string | null;
|
||||||
|
content?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
coverImage?: (number | null) | Media;
|
||||||
|
publishedAt?: string | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-kv".
|
||||||
|
*/
|
||||||
|
export interface PayloadKv {
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
data:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-locked-documents".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocument {
|
||||||
|
id: number;
|
||||||
|
document?:
|
||||||
|
| ({
|
||||||
|
relationTo: 'users';
|
||||||
|
value: number | User;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'media';
|
||||||
|
value: number | Media;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'posts';
|
||||||
|
value: number | Post;
|
||||||
|
} | null);
|
||||||
|
globalSlug?: string | null;
|
||||||
|
user: {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: number | User;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-preferences".
|
||||||
|
*/
|
||||||
|
export interface PayloadPreference {
|
||||||
|
id: number;
|
||||||
|
user: {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: number | 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: number;
|
||||||
|
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;
|
||||||
|
sessions?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
id?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
expiresAt?: T;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "media_select".
|
||||||
|
*/
|
||||||
|
export interface MediaSelect<T extends boolean = true> {
|
||||||
|
alt?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
url?: T;
|
||||||
|
thumbnailURL?: T;
|
||||||
|
filename?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
focalX?: T;
|
||||||
|
focalY?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "posts_select".
|
||||||
|
*/
|
||||||
|
export interface PostsSelect<T extends boolean = true> {
|
||||||
|
title?: T;
|
||||||
|
slug?: T;
|
||||||
|
excerpt?: T;
|
||||||
|
content?: T;
|
||||||
|
coverImage?: T;
|
||||||
|
publishedAt?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-kv_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadKvSelect<T extends boolean = true> {
|
||||||
|
key?: T;
|
||||||
|
data?: 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` "collections_widget".
|
||||||
|
*/
|
||||||
|
export interface CollectionsWidget {
|
||||||
|
data?: {
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
width: 'full';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "auth".
|
||||||
|
*/
|
||||||
|
export interface Auth {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
declare module 'payload' {
|
||||||
|
export interface GeneratedTypes extends Config {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||||
|
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||||
|
import { config as loadEnv } from 'dotenv'
|
||||||
|
import path from 'path'
|
||||||
|
import { buildConfig } from 'payload'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import sharp from 'sharp'
|
||||||
|
|
||||||
|
import { Users } from './collections/Users'
|
||||||
|
import { Media } from './collections/Media'
|
||||||
|
import { Posts } from './collections/Posts'
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
// Load the single source-of-truth env from the monorepo root (../../../.env),
|
||||||
|
// then let a local apps/cms/.env (if present) override it.
|
||||||
|
loadEnv({ path: path.resolve(dirname, '../../../.env') })
|
||||||
|
loadEnv({ path: path.resolve(dirname, '../.env'), override: true })
|
||||||
|
|
||||||
|
export default buildConfig({
|
||||||
|
admin: {
|
||||||
|
user: Users.slug,
|
||||||
|
importMap: {
|
||||||
|
baseDir: path.resolve(dirname),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collections: [Users, Media, Posts],
|
||||||
|
editor: lexicalEditor(),
|
||||||
|
secret: process.env.PAYLOAD_SECRET || '',
|
||||||
|
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||||
|
typescript: {
|
||||||
|
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||||
|
},
|
||||||
|
// Payload stores its tables in a dedicated `payload` schema so it stays
|
||||||
|
// cleanly separated from Supabase's own objects in `public`.
|
||||||
|
db: postgresAdapter({
|
||||||
|
pool: {
|
||||||
|
connectionString: process.env.DATABASE_URI || '',
|
||||||
|
},
|
||||||
|
schemaName: 'payload',
|
||||||
|
}),
|
||||||
|
sharp,
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable",
|
||||||
|
"ES2022"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
],
|
||||||
|
"@payload-config": [
|
||||||
|
"./src/payload.config.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"target": "ES2022"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export default new Map();
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export default new Map();
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
declare module 'astro:content' {
|
||||||
|
export interface RenderResult {
|
||||||
|
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
|
||||||
|
headings: import('astro').MarkdownHeading[];
|
||||||
|
remarkPluginFrontmatter: Record<string, any>;
|
||||||
|
}
|
||||||
|
interface Render {
|
||||||
|
'.md': Promise<RenderResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderedContent {
|
||||||
|
html: string;
|
||||||
|
metadata?: {
|
||||||
|
imagePaths: Array<string>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'astro:content' {
|
||||||
|
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
||||||
|
|
||||||
|
export type CollectionKey = keyof AnyEntryMap;
|
||||||
|
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
|
||||||
|
|
||||||
|
export type ContentCollectionKey = keyof ContentEntryMap;
|
||||||
|
export type DataCollectionKey = keyof DataEntryMap;
|
||||||
|
|
||||||
|
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
|
||||||
|
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
|
||||||
|
ContentEntryMap[C]
|
||||||
|
>['slug'];
|
||||||
|
|
||||||
|
export type ReferenceDataEntry<
|
||||||
|
C extends CollectionKey,
|
||||||
|
E extends keyof DataEntryMap[C] = string,
|
||||||
|
> = {
|
||||||
|
collection: C;
|
||||||
|
id: E;
|
||||||
|
};
|
||||||
|
export type ReferenceContentEntry<
|
||||||
|
C extends keyof ContentEntryMap,
|
||||||
|
E extends ValidContentEntrySlug<C> | (string & {}) = string,
|
||||||
|
> = {
|
||||||
|
collection: C;
|
||||||
|
slug: E;
|
||||||
|
};
|
||||||
|
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
|
||||||
|
collection: C;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @deprecated Use `getEntry` instead. */
|
||||||
|
export function getEntryBySlug<
|
||||||
|
C extends keyof ContentEntryMap,
|
||||||
|
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||||
|
>(
|
||||||
|
collection: C,
|
||||||
|
// Note that this has to accept a regular string too, for SSR
|
||||||
|
entrySlug: E,
|
||||||
|
): E extends ValidContentEntrySlug<C>
|
||||||
|
? Promise<CollectionEntry<C>>
|
||||||
|
: Promise<CollectionEntry<C> | undefined>;
|
||||||
|
|
||||||
|
/** @deprecated Use `getEntry` instead. */
|
||||||
|
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
|
||||||
|
collection: C,
|
||||||
|
entryId: E,
|
||||||
|
): Promise<CollectionEntry<C>>;
|
||||||
|
|
||||||
|
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
|
||||||
|
collection: C,
|
||||||
|
filter?: (entry: CollectionEntry<C>) => entry is E,
|
||||||
|
): Promise<E[]>;
|
||||||
|
export function getCollection<C extends keyof AnyEntryMap>(
|
||||||
|
collection: C,
|
||||||
|
filter?: (entry: CollectionEntry<C>) => unknown,
|
||||||
|
): Promise<CollectionEntry<C>[]>;
|
||||||
|
|
||||||
|
export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
|
||||||
|
collection: C,
|
||||||
|
filter?: LiveLoaderCollectionFilterType<C>,
|
||||||
|
): Promise<
|
||||||
|
import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function getEntry<
|
||||||
|
C extends keyof ContentEntryMap,
|
||||||
|
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||||
|
>(
|
||||||
|
entry: ReferenceContentEntry<C, E>,
|
||||||
|
): E extends ValidContentEntrySlug<C>
|
||||||
|
? Promise<CollectionEntry<C>>
|
||||||
|
: Promise<CollectionEntry<C> | undefined>;
|
||||||
|
export function getEntry<
|
||||||
|
C extends keyof DataEntryMap,
|
||||||
|
E extends keyof DataEntryMap[C] | (string & {}),
|
||||||
|
>(
|
||||||
|
entry: ReferenceDataEntry<C, E>,
|
||||||
|
): E extends keyof DataEntryMap[C]
|
||||||
|
? Promise<DataEntryMap[C][E]>
|
||||||
|
: Promise<CollectionEntry<C> | undefined>;
|
||||||
|
export function getEntry<
|
||||||
|
C extends keyof ContentEntryMap,
|
||||||
|
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||||
|
>(
|
||||||
|
collection: C,
|
||||||
|
slug: E,
|
||||||
|
): E extends ValidContentEntrySlug<C>
|
||||||
|
? Promise<CollectionEntry<C>>
|
||||||
|
: Promise<CollectionEntry<C> | undefined>;
|
||||||
|
export function getEntry<
|
||||||
|
C extends keyof DataEntryMap,
|
||||||
|
E extends keyof DataEntryMap[C] | (string & {}),
|
||||||
|
>(
|
||||||
|
collection: C,
|
||||||
|
id: E,
|
||||||
|
): E extends keyof DataEntryMap[C]
|
||||||
|
? string extends keyof DataEntryMap[C]
|
||||||
|
? Promise<DataEntryMap[C][E]> | undefined
|
||||||
|
: Promise<DataEntryMap[C][E]>
|
||||||
|
: Promise<CollectionEntry<C> | undefined>;
|
||||||
|
export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
|
||||||
|
collection: C,
|
||||||
|
filter: string | LiveLoaderEntryFilterType<C>,
|
||||||
|
): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
|
||||||
|
|
||||||
|
/** Resolve an array of entry references from the same collection */
|
||||||
|
export function getEntries<C extends keyof ContentEntryMap>(
|
||||||
|
entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[],
|
||||||
|
): Promise<CollectionEntry<C>[]>;
|
||||||
|
export function getEntries<C extends keyof DataEntryMap>(
|
||||||
|
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
|
||||||
|
): Promise<CollectionEntry<C>[]>;
|
||||||
|
|
||||||
|
export function render<C extends keyof AnyEntryMap>(
|
||||||
|
entry: AnyEntryMap[C][string],
|
||||||
|
): Promise<RenderResult>;
|
||||||
|
|
||||||
|
export function reference<C extends keyof AnyEntryMap>(
|
||||||
|
collection: C,
|
||||||
|
): import('astro/zod').ZodEffects<
|
||||||
|
import('astro/zod').ZodString,
|
||||||
|
C extends keyof ContentEntryMap
|
||||||
|
? ReferenceContentEntry<C, ValidContentEntrySlug<C>>
|
||||||
|
: ReferenceDataEntry<C, keyof DataEntryMap[C]>
|
||||||
|
>;
|
||||||
|
// Allow generic `string` to avoid excessive type errors in the config
|
||||||
|
// if `dev` is not running to update as you edit.
|
||||||
|
// Invalid collection names will be caught at build time.
|
||||||
|
export function reference<C extends string>(
|
||||||
|
collection: C,
|
||||||
|
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
|
||||||
|
|
||||||
|
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
|
||||||
|
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
|
||||||
|
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
|
||||||
|
>;
|
||||||
|
|
||||||
|
type ContentEntryMap = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
type DataEntryMap = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
type AnyEntryMap = ContentEntryMap & DataEntryMap;
|
||||||
|
|
||||||
|
type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
|
||||||
|
infer TData,
|
||||||
|
infer TEntryFilter,
|
||||||
|
infer TCollectionFilter,
|
||||||
|
infer TError
|
||||||
|
>
|
||||||
|
? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
|
||||||
|
: { data: never; entryFilter: never; collectionFilter: never; error: never };
|
||||||
|
type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
|
||||||
|
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
|
||||||
|
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
|
||||||
|
type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
|
||||||
|
|
||||||
|
type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
|
||||||
|
LiveContentConfig['collections'][C]['schema'] extends undefined
|
||||||
|
? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
|
||||||
|
: import('astro/zod').infer<
|
||||||
|
Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
|
||||||
|
>;
|
||||||
|
type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
|
||||||
|
ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
|
||||||
|
type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
|
||||||
|
ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
|
||||||
|
type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
|
||||||
|
LiveContentConfig['collections'][C]['loader']
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ContentConfig = typeof import("../src/content.config.mjs");
|
||||||
|
export type LiveContentConfig = never;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.18.2","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":false,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":\"127.0.0.1\",\"port\":4610,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\",\"entrypoint\":\"astro/assets/endpoint/dev\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[],\"actionBodySizeLimit\":1048576},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false},\"session\":{\"driver\":\"fs-lite\",\"options\":{\"base\":\"/Volumes/Data/Work/LGCC/cdrdpyj-turborepo/apps/web/node_modules/.astro/sessions\"}}}"]
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"_variables": {
|
||||||
|
"lastUpdateCheck": 1785519132161
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="astro/client" />
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Astro frontend (apps/web) — built from the monorepo root context.
|
||||||
|
# docker compose --profile apps build web
|
||||||
|
FROM node:24-slim AS base
|
||||||
|
ENV PNPM_HOME=/pnpm
|
||||||
|
ENV PATH=$PNPM_HOME:$PATH
|
||||||
|
RUN corepack enable
|
||||||
|
WORKDIR /repo
|
||||||
|
|
||||||
|
FROM base AS deps
|
||||||
|
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml* .npmrc* ./
|
||||||
|
COPY packages/typescript-config/package.json packages/typescript-config/
|
||||||
|
COPY apps/cms/package.json apps/cms/
|
||||||
|
COPY apps/web/package.json apps/web/
|
||||||
|
RUN pnpm install --frozen-lockfile || pnpm install
|
||||||
|
|
||||||
|
FROM base AS build
|
||||||
|
COPY --from=deps /repo/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm --filter web build
|
||||||
|
|
||||||
|
FROM base AS runtime
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV PORT=4321
|
||||||
|
COPY --from=build /repo/apps/web/dist ./dist
|
||||||
|
COPY --from=build /repo/apps/web/package.json ./package.json
|
||||||
|
COPY --from=build /repo/node_modules ./node_modules
|
||||||
|
EXPOSE 4321
|
||||||
|
CMD ["node", "./dist/server/entry.mjs"]
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
import node from '@astrojs/node'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
// SSR so pages can fetch live content from Payload's REST API on each request.
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'server',
|
||||||
|
adapter: node({ mode: 'standalone' }),
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
port: Number(process.env.PORT) || 4321,
|
||||||
|
},
|
||||||
|
vite: {
|
||||||
|
// Read the single root .env (shared with docker-compose) instead of a
|
||||||
|
// per-app copy. PUBLIC_* vars are exposed to the client from here.
|
||||||
|
envDir: fileURLToPath(new URL('../../', import.meta.url)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "portless web -- astro dev",
|
||||||
|
"dev:portless": "portless web -- astro dev",
|
||||||
|
"dev:plain": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"start": "node ./dist/server/entry.mjs",
|
||||||
|
"typecheck": "astro check",
|
||||||
|
"clean": "rm -rf dist .astro node_modules"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/node": "^9.1.3",
|
||||||
|
"@supabase/supabase-js": "^2.48.1",
|
||||||
|
"astro": "^5.4.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"@repo/typescript-config": "workspace:*",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
/// <reference types="astro/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly PUBLIC_PAYLOAD_API_URL?: string
|
||||||
|
readonly PUBLIC_SUPABASE_URL?: string
|
||||||
|
readonly PUBLIC_SUPABASE_ANON_KEY?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
const { title = 'cdrdpyj', description = 'Astro + Payload + Supabase' } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<title>{title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="site-header">
|
||||||
|
<a class="brand" href="/">cdrdpyj</a>
|
||||||
|
<nav>
|
||||||
|
<a href="/">Home</a>
|
||||||
|
<a href={(import.meta.env.PUBLIC_PAYLOAD_API_URL ?? '').replace(/\/api$/, '/admin')}>
|
||||||
|
Payload admin
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<footer class="site-footer">
|
||||||
|
<span>Astro frontend · content from Payload CMS · backed by Supabase</span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
:root {
|
||||||
|
--bg: #0b0c10;
|
||||||
|
--panel: #15171e;
|
||||||
|
--text: #e8eaf0;
|
||||||
|
--muted: #9aa2b1;
|
||||||
|
--accent: #6ee7b7;
|
||||||
|
--border: #262a35;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html { color-scheme: dark; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.site-header, .site-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 1rem clamp(1rem, 5vw, 4rem);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.site-footer {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
border-bottom: none;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
.brand { font-weight: 700; letter-spacing: -0.01em; }
|
||||||
|
nav { display: flex; gap: 1rem; margin-left: auto; }
|
||||||
|
a { color: var(--accent); text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
main { max-width: 760px; margin: 0 auto; padding: 2rem clamp(1rem, 5vw, 2rem); }
|
||||||
|
h1 { font-size: clamp(1.8rem, 4vw, 2.6rem); letter-spacing: -0.02em; }
|
||||||
|
.card {
|
||||||
|
display: block;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.card:hover { border-color: var(--accent); text-decoration: none; }
|
||||||
|
.card h2 { margin: 0 0 0.25rem; font-size: 1.25rem; }
|
||||||
|
.muted { color: var(--muted); }
|
||||||
|
.empty {
|
||||||
|
padding: 2rem;
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
article img { max-width: 100%; border-radius: 12px; }
|
||||||
|
blockquote {
|
||||||
|
margin: 1rem 0; padding: 0.5rem 1rem;
|
||||||
|
border-left: 3px solid var(--accent); color: var(--muted);
|
||||||
|
}
|
||||||
|
code { background: #0006; padding: 0.1em 0.35em; border-radius: 4px; }
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* Minimal serializer for Payload's Lexical rich-text JSON -> HTML.
|
||||||
|
* Handles the common node types; unknown nodes fall back to their children.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface LexicalNode {
|
||||||
|
type?: string
|
||||||
|
tag?: string
|
||||||
|
text?: string
|
||||||
|
format?: number | string
|
||||||
|
listType?: string
|
||||||
|
fields?: { url?: string; newTab?: boolean }
|
||||||
|
children?: LexicalNode[]
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
const FORMAT = { bold: 1, italic: 2, strikethrough: 4, underline: 8, code: 16 }
|
||||||
|
|
||||||
|
function escapeHtml(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderText(node: LexicalNode): string {
|
||||||
|
let out = escapeHtml(node.text ?? '')
|
||||||
|
const f = typeof node.format === 'number' ? node.format : 0
|
||||||
|
if (f & FORMAT.code) out = `<code>${out}</code>`
|
||||||
|
if (f & FORMAT.bold) out = `<strong>${out}</strong>`
|
||||||
|
if (f & FORMAT.italic) out = `<em>${out}</em>`
|
||||||
|
if (f & FORMAT.underline) out = `<u>${out}</u>`
|
||||||
|
if (f & FORMAT.strikethrough) out = `<s>${out}</s>`
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChildren(children?: LexicalNode[]): string {
|
||||||
|
if (!children) return ''
|
||||||
|
return children.map(renderNode).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNode(node: LexicalNode): string {
|
||||||
|
switch (node.type) {
|
||||||
|
case 'text':
|
||||||
|
return renderText(node)
|
||||||
|
case 'linebreak':
|
||||||
|
return '<br />'
|
||||||
|
case 'paragraph':
|
||||||
|
return `<p>${renderChildren(node.children)}</p>`
|
||||||
|
case 'heading': {
|
||||||
|
const tag = /^h[1-6]$/.test(node.tag ?? '') ? node.tag : 'h2'
|
||||||
|
return `<${tag}>${renderChildren(node.children)}</${tag}>`
|
||||||
|
}
|
||||||
|
case 'quote':
|
||||||
|
return `<blockquote>${renderChildren(node.children)}</blockquote>`
|
||||||
|
case 'list': {
|
||||||
|
const tag = node.listType === 'number' ? 'ol' : 'ul'
|
||||||
|
return `<${tag}>${renderChildren(node.children)}</${tag}>`
|
||||||
|
}
|
||||||
|
case 'listitem':
|
||||||
|
return `<li>${renderChildren(node.children)}</li>`
|
||||||
|
case 'link': {
|
||||||
|
const url = escapeHtml(node.fields?.url ?? '#')
|
||||||
|
const target = node.fields?.newTab ? ' target="_blank" rel="noopener noreferrer"' : ''
|
||||||
|
return `<a href="${url}"${target}>${renderChildren(node.children)}</a>`
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return renderChildren(node.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderLexical(content: unknown): string {
|
||||||
|
if (!content || typeof content !== 'object') return ''
|
||||||
|
const root = (content as { root?: LexicalNode }).root
|
||||||
|
if (!root?.children) return ''
|
||||||
|
return renderChildren(root.children)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/**
|
||||||
|
* Tiny typed client for Payload's REST API.
|
||||||
|
*
|
||||||
|
* Server-side (SSR) requests prefer PAYLOAD_API_URL so that, inside Docker,
|
||||||
|
* the web container can reach the cms container directly (http://cms:3000/api).
|
||||||
|
* Falls back to the public URL used by the browser.
|
||||||
|
*/
|
||||||
|
const runtimeEnv = (typeof process !== 'undefined' && process.env) || {}
|
||||||
|
const API_URL =
|
||||||
|
runtimeEnv.PAYLOAD_API_URL || // container-to-container in Docker
|
||||||
|
runtimeEnv.PUBLIC_PAYLOAD_API_URL ||
|
||||||
|
import.meta.env.PUBLIC_PAYLOAD_API_URL || // dev (loaded from root .env by Vite)
|
||||||
|
'http://localhost:3000/api'
|
||||||
|
|
||||||
|
export interface Media {
|
||||||
|
id: string
|
||||||
|
url?: string
|
||||||
|
alt?: string
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Post {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
excerpt?: string
|
||||||
|
content?: unknown
|
||||||
|
coverImage?: Media | string | null
|
||||||
|
publishedAt?: string
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaginatedDocs<T> {
|
||||||
|
docs: T[]
|
||||||
|
totalDocs: number
|
||||||
|
page: number
|
||||||
|
totalPages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiGet<T>(path: string): Promise<T> {
|
||||||
|
const res = await fetch(`${API_URL}${path}`, {
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Payload API ${res.status} ${res.statusText} for ${path}`)
|
||||||
|
}
|
||||||
|
return (await res.json()) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPosts(limit = 100): Promise<Post[]> {
|
||||||
|
const data = await apiGet<PaginatedDocs<Post>>(
|
||||||
|
`/posts?depth=1&limit=${limit}&sort=-publishedAt`,
|
||||||
|
)
|
||||||
|
return data.docs
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPostBySlug(slug: string): Promise<Post | null> {
|
||||||
|
const query = `/posts?depth=1&limit=1&where[slug][equals]=${encodeURIComponent(slug)}`
|
||||||
|
const data = await apiGet<PaginatedDocs<Post>>(query)
|
||||||
|
return data.docs[0] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mediaUrl(media: Post['coverImage']): string | null {
|
||||||
|
if (!media || typeof media === 'string') return null
|
||||||
|
if (!media.url) return null
|
||||||
|
// Payload returns root-relative URLs; make them absolute against the API host.
|
||||||
|
if (media.url.startsWith('http')) return media.url
|
||||||
|
const base = API_URL.replace(/\/api$/, '')
|
||||||
|
return `${base}${media.url}`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
import Layout from '../layouts/Layout.astro'
|
||||||
|
import { getPosts, type Post } from '../lib/payload'
|
||||||
|
|
||||||
|
let posts: Post[] = []
|
||||||
|
let error: string | null = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
posts = await getPosts()
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : String(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmt = (d?: string) =>
|
||||||
|
d ? new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : ''
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="cdrdpyj — Posts">
|
||||||
|
<h1>Posts</h1>
|
||||||
|
<p class="muted">Fetched live from the Payload CMS REST API.</p>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div class="empty">
|
||||||
|
<p><strong>Couldn't reach the CMS.</strong></p>
|
||||||
|
<p class="muted">{error}</p>
|
||||||
|
<p class="muted">Is Payload running? Try <code>pnpm dev</code> and check the CMS URL.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!error && posts.length === 0 && (
|
||||||
|
<div class="empty">
|
||||||
|
<p>No posts yet.</p>
|
||||||
|
<p class="muted">Create one in the Payload admin, then refresh.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{posts.map((post) => (
|
||||||
|
<a class="card" href={`/posts/${post.slug}`}>
|
||||||
|
<h2>{post.title}</h2>
|
||||||
|
{post.publishedAt && <div class="muted">{fmt(post.publishedAt)}</div>}
|
||||||
|
{post.excerpt && <p class="muted">{post.excerpt}</p>}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</Layout>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
import Layout from '../../layouts/Layout.astro'
|
||||||
|
import { getPostBySlug, mediaUrl } from '../../lib/payload'
|
||||||
|
import { renderLexical } from '../../lib/lexical'
|
||||||
|
|
||||||
|
const { slug } = Astro.params
|
||||||
|
|
||||||
|
const post = slug ? await getPostBySlug(slug) : null
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return new Response('Not found', { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const cover = mediaUrl(post.coverImage)
|
||||||
|
const html = renderLexical(post.content)
|
||||||
|
const fmt = (d?: string) =>
|
||||||
|
d ? new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : ''
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={`${post.title} — cdrdpyj`} description={post.excerpt}>
|
||||||
|
<p><a href="/">← All posts</a></p>
|
||||||
|
<article>
|
||||||
|
<h1>{post.title}</h1>
|
||||||
|
{post.publishedAt && <p class="muted">{fmt(post.publishedAt)}</p>}
|
||||||
|
{cover && <img src={cover} alt={post.title} />}
|
||||||
|
{post.excerpt && <p><em>{post.excerpt}</em></p>}
|
||||||
|
<Fragment set:html={html} />
|
||||||
|
</article>
|
||||||
|
</Layout>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extends": ["astro/tsconfigs/strict", "@repo/typescript-config/astro.json"],
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,354 @@
|
||||||
|
# cdrdpyj — single-file dev stack
|
||||||
|
#
|
||||||
|
# Backend only (recommended, pair with `pnpm dev` + portless on the host):
|
||||||
|
# docker compose up -d
|
||||||
|
#
|
||||||
|
# Everything in Docker (backend + Astro + Payload):
|
||||||
|
# docker compose --profile apps up -d --build
|
||||||
|
#
|
||||||
|
# Studio dashboard: http://localhost:8000 (user/pass from .env)
|
||||||
|
# Postgres: postgresql://postgres:<POSTGRES_PASSWORD>@localhost:5432/postgres
|
||||||
|
#
|
||||||
|
# The Supabase services are a trimmed self-hosted stack (db, meta, studio,
|
||||||
|
# kong, auth, rest, storage, imgproxy). Realtime / Edge Functions / Supavisor
|
||||||
|
# pooler are intentionally omitted to keep local dev light; the DB is still
|
||||||
|
# initialised with the full official role/JWT setup so they can be added later.
|
||||||
|
|
||||||
|
name: cdrdpyj
|
||||||
|
|
||||||
|
services:
|
||||||
|
#####################################################################
|
||||||
|
# Postgres — the Supabase database (also used directly by Payload CMS)
|
||||||
|
#####################################################################
|
||||||
|
db:
|
||||||
|
container_name: cdrdpyj-db
|
||||||
|
image: supabase/postgres:17.6.1.136
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# Exposed so host-run apps (Payload via portless) can connect.
|
||||||
|
- ${POSTGRES_PORT}:5432
|
||||||
|
volumes:
|
||||||
|
- ./supabase/volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
|
||||||
|
- ./supabase/volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
|
||||||
|
- ./supabase/volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
|
||||||
|
- ./supabase/volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z
|
||||||
|
# Pre-create the dedicated schema Payload CMS uses
|
||||||
|
- ./supabase/volumes/db/payload.sql:/docker-entrypoint-initdb.d/init-scripts/99-payload.sql:Z
|
||||||
|
- ./supabase/volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z
|
||||||
|
- ./supabase/volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z
|
||||||
|
- ./supabase/volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z
|
||||||
|
- ./supabase/volumes/db/data:/var/lib/postgresql/data:Z
|
||||||
|
- db-config:/etc/postgresql-custom
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "pg_isready", "-U", "postgres", "-h", "localhost"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
environment:
|
||||||
|
POSTGRES_HOST: /var/run/postgresql
|
||||||
|
PGPORT: ${POSTGRES_PORT}
|
||||||
|
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||||
|
PGPASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
PGDATABASE: ${POSTGRES_DB}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
JWT_EXP: ${JWT_EXPIRY}
|
||||||
|
command:
|
||||||
|
- postgres
|
||||||
|
- -c
|
||||||
|
- config_file=/etc/postgresql/postgresql.conf
|
||||||
|
- -c
|
||||||
|
- log_min_messages=fatal
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# postgres-meta — powers Studio's table/SQL editor
|
||||||
|
#####################################################################
|
||||||
|
meta:
|
||||||
|
container_name: cdrdpyj-meta
|
||||||
|
image: supabase/postgres-meta:v0.96.6
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
PG_META_PORT: 8080
|
||||||
|
PG_META_DB_HOST: ${POSTGRES_HOST}
|
||||||
|
PG_META_DB_PORT: ${POSTGRES_PORT}
|
||||||
|
PG_META_DB_NAME: ${POSTGRES_DB}
|
||||||
|
PG_META_DB_USER: postgres
|
||||||
|
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
CRYPTO_KEY: ${PG_META_CRYPTO_KEY}
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Studio — the Supabase dashboard
|
||||||
|
#####################################################################
|
||||||
|
studio:
|
||||||
|
container_name: cdrdpyj-studio
|
||||||
|
image: supabase/studio:2026.07.07-sha-a6a04f2
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
meta:
|
||||||
|
condition: service_started
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"node -e \"fetch('http://localhost:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})\""
|
||||||
|
]
|
||||||
|
timeout: 10s
|
||||||
|
interval: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
environment:
|
||||||
|
HOSTNAME: "0.0.0.0"
|
||||||
|
STUDIO_PG_META_URL: http://meta:8080
|
||||||
|
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||||
|
POSTGRES_HOST: ${POSTGRES_HOST}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_USER_READ_WRITE: postgres
|
||||||
|
PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY}
|
||||||
|
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
|
||||||
|
DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
|
||||||
|
DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
|
||||||
|
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||||
|
SUPABASE_URL: http://kong:8000
|
||||||
|
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||||
|
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||||
|
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||||
|
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||||
|
SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY}
|
||||||
|
SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY}
|
||||||
|
ENABLED_FEATURES_LOGS_ALL: "false"
|
||||||
|
SNIPPETS_MANAGEMENT_FOLDER: /app/snippets
|
||||||
|
EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /app/edge-functions
|
||||||
|
volumes:
|
||||||
|
- ./supabase/volumes/snippets:/app/snippets:z
|
||||||
|
- ./supabase/volumes/functions:/app/edge-functions:ro,z
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Kong — API gateway (Auth / REST / Storage all sit behind :8000)
|
||||||
|
#####################################################################
|
||||||
|
kong:
|
||||||
|
container_name: cdrdpyj-kong
|
||||||
|
image: kong/kong:3.9.1
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- api-gw
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "kong", "health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
depends_on:
|
||||||
|
studio:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- ${KONG_HTTP_PORT}:8000/tcp
|
||||||
|
- ${KONG_HTTPS_PORT}:8443/tcp
|
||||||
|
volumes:
|
||||||
|
- ./supabase/volumes/api/kong.yml:/home/kong/temp.yml:ro,z
|
||||||
|
- ./supabase/volumes/api/kong-entrypoint.sh:/home/kong/kong-entrypoint.sh:ro,z
|
||||||
|
environment:
|
||||||
|
KONG_DATABASE: "off"
|
||||||
|
KONG_DECLARATIVE_CONFIG: /usr/local/kong/kong.yml
|
||||||
|
KONG_ROUTER_FLAVOR: expressions
|
||||||
|
KONG_DNS_ORDER: LAST,A,CNAME
|
||||||
|
KONG_DNS_NOT_FOUND_TTL: 1
|
||||||
|
KONG_DNS_VALID_TTL: 5
|
||||||
|
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function
|
||||||
|
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
|
||||||
|
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
|
||||||
|
KONG_PROXY_ACCESS_LOG: /dev/stdout combined
|
||||||
|
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||||
|
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||||
|
SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:-}
|
||||||
|
SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-}
|
||||||
|
ANON_KEY_ASYMMETRIC: ${ANON_KEY_ASYMMETRIC:-}
|
||||||
|
SERVICE_ROLE_KEY_ASYMMETRIC: ${SERVICE_ROLE_KEY_ASYMMETRIC:-}
|
||||||
|
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
|
||||||
|
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
|
||||||
|
entrypoint: ["/bin/sh", "/home/kong/kong-entrypoint.sh"]
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Auth (GoTrue)
|
||||||
|
#####################################################################
|
||||||
|
auth:
|
||||||
|
container_name: cdrdpyj-auth
|
||||||
|
image: supabase/gotrue:v2.189.0
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health"]
|
||||||
|
timeout: 5s
|
||||||
|
interval: 5s
|
||||||
|
retries: 3
|
||||||
|
environment:
|
||||||
|
GOTRUE_API_HOST: 0.0.0.0
|
||||||
|
GOTRUE_API_PORT: 9999
|
||||||
|
API_EXTERNAL_URL: ${API_EXTERNAL_URL}
|
||||||
|
GOTRUE_DB_DRIVER: postgres
|
||||||
|
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||||
|
GOTRUE_SITE_URL: ${SITE_URL}
|
||||||
|
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
|
||||||
|
GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}
|
||||||
|
GOTRUE_JWT_ADMIN_ROLES: service_role
|
||||||
|
GOTRUE_JWT_AUD: authenticated
|
||||||
|
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
||||||
|
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
|
||||||
|
GOTRUE_JWT_SECRET: ${JWT_SECRET}
|
||||||
|
GOTRUE_JWT_ISSUER: ${API_EXTERNAL_URL}
|
||||||
|
GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
|
||||||
|
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
|
||||||
|
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
|
||||||
|
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL}
|
||||||
|
GOTRUE_SMTP_HOST: ${SMTP_HOST}
|
||||||
|
GOTRUE_SMTP_PORT: ${SMTP_PORT}
|
||||||
|
GOTRUE_SMTP_USER: ${SMTP_USER}
|
||||||
|
GOTRUE_SMTP_PASS: ${SMTP_PASS}
|
||||||
|
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME}
|
||||||
|
GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE}
|
||||||
|
GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION}
|
||||||
|
GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY}
|
||||||
|
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE}
|
||||||
|
GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
|
||||||
|
GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# REST (PostgREST) — auto REST API over the public schema
|
||||||
|
#####################################################################
|
||||||
|
rest:
|
||||||
|
container_name: cdrdpyj-rest
|
||||||
|
image: postgrest/postgrest:v14.12
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "postgrest", "--ready"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
environment:
|
||||||
|
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||||
|
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
|
||||||
|
PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000}
|
||||||
|
PGRST_DB_EXTRA_SEARCH_PATH: ${PGRST_DB_EXTRA_SEARCH_PATH:-public}
|
||||||
|
PGRST_DB_ANON_ROLE: anon
|
||||||
|
PGRST_ADMIN_SERVER_PORT: 3001
|
||||||
|
PGRST_ADMIN_SERVER_HOST: localhost
|
||||||
|
PGRST_JWT_SECRET: ${JWT_SECRET}
|
||||||
|
PGRST_DB_USE_LEGACY_GUCS: "false"
|
||||||
|
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
|
||||||
|
PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
|
||||||
|
command: ["postgrest"]
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Storage + imgproxy — file storage with on-the-fly image transforms
|
||||||
|
#####################################################################
|
||||||
|
storage:
|
||||||
|
container_name: cdrdpyj-storage
|
||||||
|
image: supabase/storage-api:v1.60.4
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
rest:
|
||||||
|
condition: service_started
|
||||||
|
imgproxy:
|
||||||
|
condition: service_started
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://storage:5000/status"]
|
||||||
|
timeout: 5s
|
||||||
|
interval: 5s
|
||||||
|
retries: 3
|
||||||
|
environment:
|
||||||
|
ANON_KEY: ${ANON_KEY}
|
||||||
|
SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||||
|
POSTGREST_URL: http://rest:3000
|
||||||
|
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||||
|
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||||
|
STORAGE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||||
|
REQUEST_ALLOW_X_FORWARDED_PATH: "true"
|
||||||
|
FILE_SIZE_LIMIT: 52428800
|
||||||
|
STORAGE_BACKEND: file
|
||||||
|
GLOBAL_S3_BUCKET: ${GLOBAL_S3_BUCKET}
|
||||||
|
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
|
||||||
|
TENANT_ID: ${STORAGE_TENANT_ID}
|
||||||
|
REGION: ${REGION}
|
||||||
|
ENABLE_IMAGE_TRANSFORMATION: "true"
|
||||||
|
IMGPROXY_URL: http://imgproxy:5001
|
||||||
|
S3_PROTOCOL_ACCESS_KEY_ID: ${S3_PROTOCOL_ACCESS_KEY_ID}
|
||||||
|
S3_PROTOCOL_ACCESS_KEY_SECRET: ${S3_PROTOCOL_ACCESS_KEY_SECRET}
|
||||||
|
volumes:
|
||||||
|
- ./supabase/volumes/storage:/var/lib/storage:z
|
||||||
|
|
||||||
|
imgproxy:
|
||||||
|
container_name: cdrdpyj-imgproxy
|
||||||
|
image: darthsim/imgproxy:v3.30.1
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./supabase/volumes/storage:/var/lib/storage:z
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "imgproxy", "health"]
|
||||||
|
timeout: 5s
|
||||||
|
interval: 5s
|
||||||
|
retries: 3
|
||||||
|
environment:
|
||||||
|
IMGPROXY_BIND: ":5001"
|
||||||
|
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
|
||||||
|
IMGPROXY_USE_ETAG: "true"
|
||||||
|
IMGPROXY_AUTO_WEBP: ${IMGPROXY_AUTO_WEBP}
|
||||||
|
IMGPROXY_MAX_SRC_RESOLUTION: 16.8
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Applications (opt-in: `docker compose --profile apps up`)
|
||||||
|
# The recommended dev loop runs these on the host via portless
|
||||||
|
# (`pnpm dev`) instead, for hot-reload + *.localhost URLs.
|
||||||
|
#####################################################################
|
||||||
|
cms:
|
||||||
|
container_name: cdrdpyj-cms
|
||||||
|
profiles: ["apps"]
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apps/cms/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
DATABASE_URI: postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||||
|
PAYLOAD_SECRET: ${PAYLOAD_SECRET}
|
||||||
|
PAYLOAD_PUBLIC_SERVER_URL: http://localhost:3000
|
||||||
|
|
||||||
|
web:
|
||||||
|
container_name: cdrdpyj-web
|
||||||
|
profiles: ["apps"]
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apps/web/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- cms
|
||||||
|
ports:
|
||||||
|
- "4321:4321"
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
PORT: 4321
|
||||||
|
# Server-side fetches go container-to-container; browser links use localhost.
|
||||||
|
PAYLOAD_API_URL: http://cms:3000/api
|
||||||
|
PUBLIC_PAYLOAD_API_URL: http://localhost:3000/api
|
||||||
|
PUBLIC_SUPABASE_URL: ${SUPABASE_PUBLIC_URL}
|
||||||
|
PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-config:
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "cdrdpyj-turborepo",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@11.9.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "turbo run dev",
|
||||||
|
"dev:portless": "turbo run dev:portless",
|
||||||
|
"build": "turbo run build",
|
||||||
|
"start": "turbo run start",
|
||||||
|
"lint": "turbo run lint",
|
||||||
|
"typecheck": "turbo run typecheck",
|
||||||
|
"clean": "turbo run clean && rm -rf node_modules",
|
||||||
|
"supabase:up": "docker compose up -d",
|
||||||
|
"supabase:down": "docker compose down",
|
||||||
|
"supabase:logs": "docker compose logs -f",
|
||||||
|
"supabase:reset": "docker compose down -v && rm -rf supabase/volumes/db/data",
|
||||||
|
"stack:up": "docker compose --profile apps up -d --build",
|
||||||
|
"stack:down": "docker compose --profile apps down"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"portless": "^0.15.5",
|
||||||
|
"turbo": "^2.5.4",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "Astro",
|
||||||
|
"extends": "./base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "astro",
|
||||||
|
"noEmit": true,
|
||||||
|
"allowJs": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "Base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"incremental": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "Next.js",
|
||||||
|
"extends": "./base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"noEmit": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"plugins": [{ "name": "next" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "@repo/typescript-config",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"files": [
|
||||||
|
"base.json",
|
||||||
|
"nextjs.json",
|
||||||
|
"astro.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,22 @@
|
||||||
|
packages:
|
||||||
|
- "apps/*"
|
||||||
|
- "packages/*"
|
||||||
|
|
||||||
|
# Native/postinstall build scripts we trust to run.
|
||||||
|
allowBuilds:
|
||||||
|
"@parcel/watcher": true
|
||||||
|
esbuild: true
|
||||||
|
sharp: true
|
||||||
|
unrs-resolver: true
|
||||||
|
|
||||||
|
# pnpm's minimum-release-age policy gates very recently published packages;
|
||||||
|
# these pins are allowed through so the Payload 3.87.0 set installs.
|
||||||
|
minimumReleaseAgeExclude:
|
||||||
|
- "@payloadcms/db-postgres@3.87.0"
|
||||||
|
- "@payloadcms/drizzle@3.87.0"
|
||||||
|
- "@payloadcms/graphql@3.87.0"
|
||||||
|
- "@payloadcms/next@3.87.0"
|
||||||
|
- "@payloadcms/richtext-lexical@3.87.0"
|
||||||
|
- "@payloadcms/translations@3.87.0"
|
||||||
|
- "@payloadcms/ui@3.87.0"
|
||||||
|
- payload@3.87.0
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Custom entrypoint for Kong that builds Lua expressions for request-transformer
|
||||||
|
# and performs environment variable substitution in the declarative config.
|
||||||
|
|
||||||
|
# Build Lua expressions for translating opaque API keys to asymmetric JWTs.
|
||||||
|
# When opaque keys are not configured (empty env vars), expressions fall through
|
||||||
|
# to legacy-only behavior - just passing apikey as-is.
|
||||||
|
#
|
||||||
|
# Full expression logic (when opaque keys are configured):
|
||||||
|
# 1. If Authorization header exists and is NOT an sb_ key -> pass through (user session JWT)
|
||||||
|
# 2. If apikey matches secret key -> set service_role asymmetric JWT internal "API key"
|
||||||
|
# 3. If apikey matches publishable key -> set anon asymmetric JWT internal "API key"
|
||||||
|
# 4. Fallback: pass apikey as-is (legacy HS256 JWT)
|
||||||
|
|
||||||
|
if [ -n "$SUPABASE_SECRET_KEY" ] && [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||||
|
# Opaque keys configured -> full translation expressions
|
||||||
|
export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or (headers.apikey == '$SUPABASE_SECRET_KEY' and 'Bearer $SERVICE_ROLE_KEY_ASYMMETRIC') or (headers.apikey == '$SUPABASE_PUBLISHABLE_KEY' and 'Bearer $ANON_KEY_ASYMMETRIC') or headers.apikey)"
|
||||||
|
|
||||||
|
# Realtime WebSocket: reads from query_params.apikey (supabase-js sends apikey
|
||||||
|
# via query string), outputs to x-api-key header which Realtime checks first.
|
||||||
|
export LUA_RT_WS_EXPR="\$((query_params.apikey == '$SUPABASE_SECRET_KEY' and '$SERVICE_ROLE_KEY_ASYMMETRIC') or (query_params.apikey == '$SUPABASE_PUBLISHABLE_KEY' and '$ANON_KEY_ASYMMETRIC') or query_params.apikey)"
|
||||||
|
else
|
||||||
|
# Legacy API keys, not sb_ API keys -> pass apikey through unchanged
|
||||||
|
export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or headers.apikey)"
|
||||||
|
export LUA_RT_WS_EXPR="\$(query_params.apikey)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Substitute environment variables in the Kong declarative config.
|
||||||
|
# Uses awk instead of eval/echo to preserve YAML quoting (eval strips double
|
||||||
|
# quotes, breaking "Header: value" patterns that YAML parses as mappings).
|
||||||
|
awk '{
|
||||||
|
result = ""
|
||||||
|
rest = $0
|
||||||
|
while (match(rest, /\$[A-Za-z_][A-Za-z_0-9]*/)) {
|
||||||
|
varname = substr(rest, RSTART + 1, RLENGTH - 1)
|
||||||
|
if (varname in ENVIRON) {
|
||||||
|
result = result substr(rest, 1, RSTART - 1) ENVIRON[varname]
|
||||||
|
} else {
|
||||||
|
result = result substr(rest, 1, RSTART + RLENGTH - 1)
|
||||||
|
}
|
||||||
|
rest = substr(rest, RSTART + RLENGTH)
|
||||||
|
}
|
||||||
|
print result rest
|
||||||
|
}' /home/kong/temp.yml > "$KONG_DECLARATIVE_CONFIG"
|
||||||
|
|
||||||
|
# Remove empty key-auth credentials (unconfigured opaque keys)
|
||||||
|
sed -i '/^[[:space:]]*- key:[[:space:]]*$/d' "$KONG_DECLARATIVE_CONFIG"
|
||||||
|
|
||||||
|
exec /entrypoint.sh kong docker-start
|
||||||
|
|
@ -0,0 +1,467 @@
|
||||||
|
_format_version: '2.1'
|
||||||
|
_transform: true
|
||||||
|
|
||||||
|
###
|
||||||
|
### Consumers / Users
|
||||||
|
###
|
||||||
|
consumers:
|
||||||
|
- username: DASHBOARD
|
||||||
|
- username: anon
|
||||||
|
keyauth_credentials:
|
||||||
|
- key: $SUPABASE_ANON_KEY
|
||||||
|
- key: $SUPABASE_PUBLISHABLE_KEY
|
||||||
|
- username: service_role
|
||||||
|
keyauth_credentials:
|
||||||
|
- key: $SUPABASE_SERVICE_KEY
|
||||||
|
- key: $SUPABASE_SECRET_KEY
|
||||||
|
|
||||||
|
###
|
||||||
|
### Access Control List
|
||||||
|
###
|
||||||
|
acls:
|
||||||
|
- consumer: anon
|
||||||
|
group: anon
|
||||||
|
- consumer: service_role
|
||||||
|
group: admin
|
||||||
|
|
||||||
|
###
|
||||||
|
### Dashboard credentials
|
||||||
|
###
|
||||||
|
basicauth_credentials:
|
||||||
|
- consumer: DASHBOARD
|
||||||
|
username: '$DASHBOARD_USERNAME'
|
||||||
|
password: '$DASHBOARD_PASSWORD'
|
||||||
|
|
||||||
|
###
|
||||||
|
### API Routes
|
||||||
|
###
|
||||||
|
services:
|
||||||
|
## Open Auth routes
|
||||||
|
- name: auth-v1-open
|
||||||
|
_comment: 'Auth: /auth/v1/verify* -> http://auth:9999/verify*'
|
||||||
|
url: http://auth:9999/verify
|
||||||
|
routes:
|
||||||
|
- name: auth-v1-open
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /auth/v1/verify
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
- name: auth-v1-open-callback
|
||||||
|
_comment: 'Auth: /auth/v1/callback* -> http://auth:9999/callback*'
|
||||||
|
url: http://auth:9999/callback
|
||||||
|
routes:
|
||||||
|
- name: auth-v1-open-callback
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /auth/v1/callback
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
- name: auth-v1-open-authorize
|
||||||
|
_comment: 'Auth: /auth/v1/authorize* -> http://auth:9999/authorize*'
|
||||||
|
url: http://auth:9999/authorize
|
||||||
|
routes:
|
||||||
|
- name: auth-v1-open-authorize
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /auth/v1/authorize
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
- name: auth-v1-open-jwks
|
||||||
|
_comment: 'Auth: /auth/v1/.well-known/jwks.json -> http://auth:9999/.well-known/jwks.json'
|
||||||
|
url: http://auth:9999/.well-known/jwks.json
|
||||||
|
routes:
|
||||||
|
- name: auth-v1-open-jwks
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /auth/v1/.well-known/jwks.json
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
|
||||||
|
- name: auth-v1-open-sso-acs
|
||||||
|
url: "http://auth:9999/sso/saml/acs"
|
||||||
|
routes:
|
||||||
|
- name: auth-v1-open-sso-acs
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /auth/v1/sso/saml/acs
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
|
||||||
|
- name: auth-v1-open-sso-metadata
|
||||||
|
url: "http://auth:9999/sso/saml/metadata"
|
||||||
|
routes:
|
||||||
|
- name: auth-v1-open-sso-metadata
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /auth/v1/sso/saml/metadata
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
|
||||||
|
## Secure Auth routes
|
||||||
|
- name: auth-v1
|
||||||
|
_comment: 'Auth: /auth/v1/* -> http://auth:9999/*'
|
||||||
|
url: http://auth:9999/
|
||||||
|
routes:
|
||||||
|
- name: auth-v1-all
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /auth/v1/
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
- name: key-auth
|
||||||
|
config:
|
||||||
|
hide_credentials: false
|
||||||
|
- name: request-transformer
|
||||||
|
config:
|
||||||
|
add:
|
||||||
|
headers:
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
replace:
|
||||||
|
headers:
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
- name: acl
|
||||||
|
config:
|
||||||
|
hide_groups_header: true
|
||||||
|
allow:
|
||||||
|
- admin
|
||||||
|
- anon
|
||||||
|
|
||||||
|
## OpenAPI root - admin only
|
||||||
|
- name: rest-v1-openapi
|
||||||
|
_comment: 'PostgREST OpenAPI root: /rest/v1/ -> <http://rest:3000/> (admin only). See <https://github.com/orgs/supabase/discussions/42949>'
|
||||||
|
url: http://rest:3000/
|
||||||
|
routes:
|
||||||
|
- name: rest-v1-openapi-root
|
||||||
|
strip_path: true
|
||||||
|
expression: 'http.path == "/rest/v1/"'
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
- name: key-auth
|
||||||
|
config:
|
||||||
|
hide_credentials: false
|
||||||
|
- name: request-transformer
|
||||||
|
config:
|
||||||
|
add:
|
||||||
|
headers:
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
replace:
|
||||||
|
headers:
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
- name: acl
|
||||||
|
config:
|
||||||
|
hide_groups_header: true
|
||||||
|
allow:
|
||||||
|
- admin
|
||||||
|
|
||||||
|
## Secure PostgREST routes
|
||||||
|
- name: rest-v1
|
||||||
|
_comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
|
||||||
|
url: http://rest:3000/
|
||||||
|
routes:
|
||||||
|
- name: rest-v1-all
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /rest/v1/
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
- name: key-auth
|
||||||
|
config:
|
||||||
|
hide_credentials: false
|
||||||
|
- name: request-transformer
|
||||||
|
config:
|
||||||
|
add:
|
||||||
|
headers:
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
replace:
|
||||||
|
headers:
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
- name: acl
|
||||||
|
config:
|
||||||
|
hide_groups_header: true
|
||||||
|
allow:
|
||||||
|
- admin
|
||||||
|
- anon
|
||||||
|
|
||||||
|
## Secure GraphQL routes
|
||||||
|
- name: graphql-v1
|
||||||
|
_comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql'
|
||||||
|
url: http://rest:3000/rpc/graphql
|
||||||
|
routes:
|
||||||
|
- name: graphql-v1-all
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /graphql/v1
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
- name: key-auth
|
||||||
|
config:
|
||||||
|
hide_credentials: false
|
||||||
|
- name: request-transformer
|
||||||
|
config:
|
||||||
|
add:
|
||||||
|
headers:
|
||||||
|
- "Content-Profile: graphql_public"
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
replace:
|
||||||
|
headers:
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
- name: acl
|
||||||
|
config:
|
||||||
|
hide_groups_header: true
|
||||||
|
allow:
|
||||||
|
- admin
|
||||||
|
- anon
|
||||||
|
|
||||||
|
## Secure Realtime routes
|
||||||
|
- name: realtime-v1-ws
|
||||||
|
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
|
||||||
|
url: http://realtime-dev.supabase-realtime:4000/socket
|
||||||
|
protocol: ws
|
||||||
|
routes:
|
||||||
|
- name: realtime-v1-ws
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /realtime/v1/
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
- name: key-auth
|
||||||
|
config:
|
||||||
|
hide_credentials: false
|
||||||
|
- name: request-transformer
|
||||||
|
config:
|
||||||
|
add:
|
||||||
|
headers:
|
||||||
|
- "x-api-key:$LUA_RT_WS_EXPR"
|
||||||
|
replace:
|
||||||
|
querystring:
|
||||||
|
- "apikey:$LUA_RT_WS_EXPR"
|
||||||
|
- name: acl
|
||||||
|
config:
|
||||||
|
hide_groups_header: true
|
||||||
|
allow:
|
||||||
|
- admin
|
||||||
|
- anon
|
||||||
|
|
||||||
|
# Block access to /realtime/v1/api/openapi
|
||||||
|
- name: realtime-v1-rest-openapi
|
||||||
|
_comment: 'Realtime: /realtime/v1/api/openapi/* -> http://realtime:4000/api/openapi/* (blocked)'
|
||||||
|
url: http://realtime-dev.supabase-realtime:4000/api/openapi
|
||||||
|
protocol: http
|
||||||
|
routes:
|
||||||
|
- name: realtime-v1-rest-openapi
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /realtime/v1/api/openapi
|
||||||
|
plugins:
|
||||||
|
- name: request-termination
|
||||||
|
config:
|
||||||
|
status_code: 403
|
||||||
|
message: "Access is forbidden."
|
||||||
|
|
||||||
|
# Block access to /realtime/v1/api/tenants
|
||||||
|
- name: realtime-v1-rest-tenants
|
||||||
|
_comment: 'Realtime: /realtime/v1/api/tenants/* -> http://realtime:4000/api/tenants/* (blocked)'
|
||||||
|
url: http://realtime-dev.supabase-realtime:4000/api/tenants
|
||||||
|
protocol: http
|
||||||
|
routes:
|
||||||
|
- name: realtime-v1-rest-tenants
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /realtime/v1/api/tenants
|
||||||
|
plugins:
|
||||||
|
- name: request-termination
|
||||||
|
config:
|
||||||
|
status_code: 403
|
||||||
|
message: "Access is forbidden."
|
||||||
|
|
||||||
|
- name: realtime-v1-rest
|
||||||
|
_comment: 'Realtime: /realtime/v1/api/* -> http://realtime:4000/api/*'
|
||||||
|
url: http://realtime-dev.supabase-realtime:4000/api
|
||||||
|
protocol: http
|
||||||
|
routes:
|
||||||
|
- name: realtime-v1-rest
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /realtime/v1/api
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
- name: key-auth
|
||||||
|
config:
|
||||||
|
hide_credentials: false
|
||||||
|
- name: request-transformer
|
||||||
|
config:
|
||||||
|
add:
|
||||||
|
headers:
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
replace:
|
||||||
|
headers:
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
- name: acl
|
||||||
|
config:
|
||||||
|
hide_groups_header: true
|
||||||
|
allow:
|
||||||
|
- admin
|
||||||
|
- anon
|
||||||
|
|
||||||
|
## Storage API endpoint (with Authorization header transformation).
|
||||||
|
## No key-auth - S3 protocol requests don't carry an apikey header.
|
||||||
|
##
|
||||||
|
## The request-transformer translates opaque API keys to asymmetric JWTs
|
||||||
|
## and passes through existing Authorization headers (user JWTs, AWS SigV4).
|
||||||
|
## When no Authorization or apikey header is present (S3 presigned URLs),
|
||||||
|
## the Lua expression evaluates to nil which Kong renders as empty string.
|
||||||
|
## The post-function strips this empty header so Storage's S3 signature
|
||||||
|
## verification falls through to query-parameter parsing.
|
||||||
|
- name: storage-v1
|
||||||
|
_comment: 'Storage: /storage/v1/* -> http://storage:5000/*'
|
||||||
|
url: http://storage:5000/
|
||||||
|
routes:
|
||||||
|
- name: storage-v1-all
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /storage/v1/
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
- name: request-transformer
|
||||||
|
config:
|
||||||
|
add:
|
||||||
|
headers:
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
replace:
|
||||||
|
headers:
|
||||||
|
- "Authorization: $LUA_AUTH_EXPR"
|
||||||
|
- name: post-function
|
||||||
|
config:
|
||||||
|
access:
|
||||||
|
- |
|
||||||
|
local auth = kong.request.get_header("authorization")
|
||||||
|
if auth == nil or auth == "" or auth:find("^%s*$") then
|
||||||
|
kong.service.request.clear_header("authorization")
|
||||||
|
end
|
||||||
|
|
||||||
|
## Edge Functions routes
|
||||||
|
- name: functions-v1
|
||||||
|
_comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*'
|
||||||
|
url: http://functions:9000/
|
||||||
|
read_timeout: 150000
|
||||||
|
routes:
|
||||||
|
- name: functions-v1-all
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /functions/v1/
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
|
||||||
|
## OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
||||||
|
- name: well-known-oauth
|
||||||
|
_comment: 'Auth: /.well-known/oauth-authorization-server -> http://auth:9999/.well-known/oauth-authorization-server'
|
||||||
|
url: http://auth:9999/.well-known/oauth-authorization-server
|
||||||
|
routes:
|
||||||
|
- name: well-known-oauth
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /.well-known/oauth-authorization-server
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
|
||||||
|
## Analytics routes
|
||||||
|
## Not used - Studio and Vector talk directly to analytics via Docker networking.
|
||||||
|
## If external access is needed, add routes with key-auth matching Logflare's x-api-key auth.
|
||||||
|
# - name: analytics-v1-api
|
||||||
|
# _comment: 'Analytics: /analytics/v1/api/endpoints/* -> http://logflare:4000/api/endpoints/*'
|
||||||
|
# url: http://analytics:4000/api/endpoints
|
||||||
|
# routes:
|
||||||
|
# - name: analytics-v1-api
|
||||||
|
# strip_path: true
|
||||||
|
# paths:
|
||||||
|
# - /analytics/v1/api/endpoints/
|
||||||
|
# - name: analytics-v1
|
||||||
|
# _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*'
|
||||||
|
# url: http://analytics:4000/
|
||||||
|
# routes:
|
||||||
|
# - name: dashboard-v1-all
|
||||||
|
# strip_path: true
|
||||||
|
# paths:
|
||||||
|
# - /analytics/v1
|
||||||
|
# plugins:
|
||||||
|
# - name: cors
|
||||||
|
# - name: basic-auth
|
||||||
|
# config:
|
||||||
|
# hide_credentials: true
|
||||||
|
|
||||||
|
## Secure Database routes
|
||||||
|
- name: meta
|
||||||
|
_comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*'
|
||||||
|
url: http://meta:8080/
|
||||||
|
routes:
|
||||||
|
- name: meta-all
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /pg/
|
||||||
|
plugins:
|
||||||
|
- name: key-auth
|
||||||
|
config:
|
||||||
|
hide_credentials: false
|
||||||
|
- name: acl
|
||||||
|
config:
|
||||||
|
hide_groups_header: true
|
||||||
|
allow:
|
||||||
|
- admin
|
||||||
|
|
||||||
|
## Block access to /api/mcp
|
||||||
|
- name: mcp-blocker
|
||||||
|
_comment: 'Block direct access to /api/mcp'
|
||||||
|
url: http://studio:3000/api/mcp
|
||||||
|
routes:
|
||||||
|
- name: mcp-blocker-route
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /api/mcp
|
||||||
|
plugins:
|
||||||
|
- name: request-termination
|
||||||
|
config:
|
||||||
|
status_code: 403
|
||||||
|
message: "Access is forbidden."
|
||||||
|
|
||||||
|
## MCP endpoint - local access
|
||||||
|
- name: mcp
|
||||||
|
_comment: 'MCP: /mcp -> http://studio:3000/api/mcp (local access)'
|
||||||
|
url: http://studio:3000/api/mcp
|
||||||
|
routes:
|
||||||
|
- name: mcp
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /mcp
|
||||||
|
plugins:
|
||||||
|
# Block access to /mcp by default
|
||||||
|
- name: request-termination
|
||||||
|
config:
|
||||||
|
status_code: 403
|
||||||
|
message: "Access is forbidden."
|
||||||
|
# Enable local access (danger zone!)
|
||||||
|
# 1. Comment out the 'request-termination' section above
|
||||||
|
# 2. Uncomment the entire section below, including 'deny'
|
||||||
|
# 3. Add your local IPs to the 'allow' list
|
||||||
|
#- name: cors
|
||||||
|
#- name: ip-restriction
|
||||||
|
# config:
|
||||||
|
# allow:
|
||||||
|
# - 127.0.0.1
|
||||||
|
# - ::1
|
||||||
|
# deny: []
|
||||||
|
|
||||||
|
## Protected Dashboard - catch all remaining routes
|
||||||
|
- name: dashboard
|
||||||
|
_comment: 'Studio: /* -> http://studio:3000/*'
|
||||||
|
url: http://studio:3000/
|
||||||
|
routes:
|
||||||
|
- name: dashboard-all
|
||||||
|
strip_path: true
|
||||||
|
paths:
|
||||||
|
- /
|
||||||
|
plugins:
|
||||||
|
- name: cors
|
||||||
|
- name: basic-auth
|
||||||
|
config:
|
||||||
|
hide_credentials: true
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
\set pguser `echo "$POSTGRES_USER"`
|
||||||
|
|
||||||
|
CREATE DATABASE _supabase WITH OWNER :pguser;
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
\set jwt_secret `echo "$JWT_SECRET"`
|
||||||
|
\set jwt_exp `echo "$JWT_EXP"`
|
||||||
|
|
||||||
|
ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret';
|
||||||
|
ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp';
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
\set pguser `echo "$POSTGRES_USER"`
|
||||||
|
|
||||||
|
\c _supabase
|
||||||
|
create schema if not exists _analytics;
|
||||||
|
alter schema _analytics owner to :pguser;
|
||||||
|
\c postgres
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
-- Dedicated schema for Payload CMS tables, kept separate from Supabase's
|
||||||
|
-- own objects in `public`. Payload connects as `postgres` and will also
|
||||||
|
-- create this on first boot; pre-creating it here avoids any edge cases.
|
||||||
|
CREATE SCHEMA IF NOT EXISTS payload AUTHORIZATION postgres;
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
\set pguser `echo "$POSTGRES_USER"`
|
||||||
|
|
||||||
|
\c _supabase
|
||||||
|
create schema if not exists _supavisor;
|
||||||
|
alter schema _supavisor owner to :pguser;
|
||||||
|
\c postgres
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
\set pguser `echo "$POSTGRES_USER"`
|
||||||
|
|
||||||
|
create schema if not exists _realtime;
|
||||||
|
alter schema _realtime owner to :pguser;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- NOTE: change to your own passwords for production environments
|
||||||
|
\set pgpass `echo "$POSTGRES_PASSWORD"`
|
||||||
|
|
||||||
|
ALTER USER authenticator WITH PASSWORD :'pgpass';
|
||||||
|
ALTER USER pgbouncer WITH PASSWORD :'pgpass';
|
||||||
|
ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass';
|
||||||
|
ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass';
|
||||||
|
ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass';
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
BEGIN;
|
||||||
|
-- Create pg_net extension
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;
|
||||||
|
-- Create supabase_functions schema
|
||||||
|
CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin;
|
||||||
|
GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
|
||||||
|
-- supabase_functions.migrations definition
|
||||||
|
CREATE TABLE supabase_functions.migrations (
|
||||||
|
version text PRIMARY KEY,
|
||||||
|
inserted_at timestamptz NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
-- Initial supabase_functions migration
|
||||||
|
INSERT INTO supabase_functions.migrations (version) VALUES ('initial');
|
||||||
|
-- supabase_functions.hooks definition
|
||||||
|
CREATE TABLE supabase_functions.hooks (
|
||||||
|
id bigserial PRIMARY KEY,
|
||||||
|
hook_table_id integer NOT NULL,
|
||||||
|
hook_name text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT NOW(),
|
||||||
|
request_id bigint
|
||||||
|
);
|
||||||
|
CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id);
|
||||||
|
CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name);
|
||||||
|
COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
|
||||||
|
CREATE FUNCTION supabase_functions.http_request()
|
||||||
|
RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
request_id bigint;
|
||||||
|
payload jsonb;
|
||||||
|
url text := TG_ARGV[0]::text;
|
||||||
|
method text := TG_ARGV[1]::text;
|
||||||
|
headers jsonb DEFAULT '{}'::jsonb;
|
||||||
|
params jsonb DEFAULT '{}'::jsonb;
|
||||||
|
timeout_ms integer DEFAULT 1000;
|
||||||
|
BEGIN
|
||||||
|
IF url IS NULL OR url = 'null' THEN
|
||||||
|
RAISE EXCEPTION 'url argument is missing';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF method IS NULL OR method = 'null' THEN
|
||||||
|
RAISE EXCEPTION 'method argument is missing';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
|
||||||
|
headers = '{"Content-Type": "application/json"}'::jsonb;
|
||||||
|
ELSE
|
||||||
|
headers = TG_ARGV[2]::jsonb;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
|
||||||
|
params = '{}'::jsonb;
|
||||||
|
ELSE
|
||||||
|
params = TG_ARGV[3]::jsonb;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
|
||||||
|
timeout_ms = 1000;
|
||||||
|
ELSE
|
||||||
|
timeout_ms = TG_ARGV[4]::integer;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN method = 'GET' THEN
|
||||||
|
SELECT http_get INTO request_id FROM net.http_get(
|
||||||
|
url,
|
||||||
|
params,
|
||||||
|
headers,
|
||||||
|
timeout_ms
|
||||||
|
);
|
||||||
|
WHEN method = 'POST' THEN
|
||||||
|
payload = jsonb_build_object(
|
||||||
|
'old_record', OLD,
|
||||||
|
'record', NEW,
|
||||||
|
'type', TG_OP,
|
||||||
|
'table', TG_TABLE_NAME,
|
||||||
|
'schema', TG_TABLE_SCHEMA
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT http_post INTO request_id FROM net.http_post(
|
||||||
|
url,
|
||||||
|
payload,
|
||||||
|
params,
|
||||||
|
headers,
|
||||||
|
timeout_ms
|
||||||
|
);
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION 'method argument % is invalid', method;
|
||||||
|
END CASE;
|
||||||
|
|
||||||
|
INSERT INTO supabase_functions.hooks
|
||||||
|
(hook_table_id, hook_name, request_id)
|
||||||
|
VALUES
|
||||||
|
(TG_RELID, TG_NAME, request_id);
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$function$;
|
||||||
|
-- Supabase super admin
|
||||||
|
DO
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_roles
|
||||||
|
WHERE rolname = 'supabase_functions_admin'
|
||||||
|
)
|
||||||
|
THEN
|
||||||
|
CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
||||||
|
ALTER USER supabase_functions_admin SET search_path = "supabase_functions";
|
||||||
|
ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin;
|
||||||
|
ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin;
|
||||||
|
ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin;
|
||||||
|
GRANT supabase_functions_admin TO postgres;
|
||||||
|
-- Remove unused supabase_pg_net_admin role
|
||||||
|
DO
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_roles
|
||||||
|
WHERE rolname = 'supabase_pg_net_admin'
|
||||||
|
)
|
||||||
|
THEN
|
||||||
|
REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin;
|
||||||
|
DROP OWNED BY supabase_pg_net_admin;
|
||||||
|
DROP ROLE supabase_pg_net_admin;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
-- pg_net grants when extension is already enabled
|
||||||
|
DO
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_extension
|
||||||
|
WHERE extname = 'pg_net'
|
||||||
|
)
|
||||||
|
THEN
|
||||||
|
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||||
|
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||||
|
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||||
|
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||||
|
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||||
|
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||||
|
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||||
|
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||||
|
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
-- Event trigger for pg_net
|
||||||
|
CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
|
||||||
|
RETURNS event_trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_event_trigger_ddl_commands() AS ev
|
||||||
|
JOIN pg_extension AS ext
|
||||||
|
ON ev.objid = ext.oid
|
||||||
|
WHERE ext.extname = 'pg_net'
|
||||||
|
)
|
||||||
|
THEN
|
||||||
|
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||||
|
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||||
|
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||||
|
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||||
|
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||||
|
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||||
|
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||||
|
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||||
|
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net';
|
||||||
|
DO
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_event_trigger
|
||||||
|
WHERE evtname = 'issue_pg_net_access'
|
||||||
|
) THEN
|
||||||
|
CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION')
|
||||||
|
EXECUTE PROCEDURE extensions.grant_pg_net_access();
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants');
|
||||||
|
ALTER function supabase_functions.http_request() SECURITY DEFINER;
|
||||||
|
ALTER function supabase_functions.http_request() SET search_path = supabase_functions;
|
||||||
|
REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;
|
||||||
|
GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role;
|
||||||
|
COMMIT;
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"ui": "tui",
|
||||||
|
"globalDependencies": [".env"],
|
||||||
|
"globalEnv": [
|
||||||
|
"NODE_ENV",
|
||||||
|
"DATABASE_URI",
|
||||||
|
"PAYLOAD_SECRET",
|
||||||
|
"PAYLOAD_PUBLIC_SERVER_URL",
|
||||||
|
"PUBLIC_PAYLOAD_API_URL",
|
||||||
|
"SUPABASE_URL",
|
||||||
|
"SUPABASE_ANON_KEY",
|
||||||
|
"PORT"
|
||||||
|
],
|
||||||
|
"tasks": {
|
||||||
|
"dev": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"dev:portless": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"dependsOn": ["build"],
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"typecheck": {
|
||||||
|
"dependsOn": ["^build"]
|
||||||
|
},
|
||||||
|
"lint": {},
|
||||||
|
"clean": {
|
||||||
|
"cache": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue