diff --git a/.env-min.example b/.env-min.example index 0a90a8c1..a4f0dbe0 100644 --- a/.env-min.example +++ b/.env-min.example @@ -1,2 +1,3 @@ KENER_SECRET_KEY=some_secret_key_for_kener REDIS_URL=redis://localhost:6379 +ORIGIN=http://localhost:3000 diff --git a/.github/workflows/publish-main.yml b/.github/workflows/publish-main.yml new file mode 100644 index 00000000..0560acb2 --- /dev/null +++ b/.github/workflows/publish-main.yml @@ -0,0 +1,92 @@ +name: Publish Main Docker Image (with Docs) + +on: + push: + branches: + - main + workflow_dispatch: + +env: + DOCKERHUB_REGISTRY: docker.io + GITHUB_REGISTRY: ghcr.io + DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} + GITHUB_IMAGE_NAME: ${{ github.repository }} + +concurrency: + group: main-docker-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-push-main: + name: Build and push main Docker images (with docs) + strategy: + matrix: + variant: [debian, alpine] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Check out the repo + uses: actions/checkout@v4.2.2 + + - name: Install cosign + uses: sigstore/cosign-installer@v3.8.0 + with: + cosign-release: v2.2.4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.8.0 + + - name: Log in to Docker Hub + uses: docker/login-action@v3.3.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3.3.0 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5.6.1 + with: + images: | + ${{ env.DOCKERHUB_IMAGE_NAME }} + ${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME }} + tags: | + type=raw,value=main-with-docs,enable=${{ matrix.variant == 'debian' }} + type=raw,value=main-with-docs-alpine,enable=${{ matrix.variant == 'alpine' }} + type=sha,format=short,prefix=main-with-docs-,enable=${{ matrix.variant == 'debian' }} + type=sha,format=short,prefix=main-with-docs-alpine-,enable=${{ matrix.variant == 'alpine' }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.3.0 + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6.13.0 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VARIANT=${{ matrix.variant }} + WITH_DOCS=true + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Sign the published Docker images + env: + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + run: | + echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/Dockerfile b/Dockerfile index ae2f9689..46b02e06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,12 @@ # Build: # docker build -t kener . # Alpine (default) # docker build -t kener --build-arg VARIANT=debian . # Debian Slim +# docker build -t kener --build-arg WITH_DOCS=true . # Include docs # # Run: # docker run -d -p 3000:3000 \ # -e KENER_SECRET_KEY= \ +# -e ORIGIN=http://localhost:3000 \ # -e REDIS_URL=redis://:6379 \ # -v kener_db:/app/database \ # kener @@ -18,6 +20,7 @@ ARG NODE_VERSION=24 ARG VARIANT=alpine +ARG WITH_DOCS=false # ============================================================================= # STAGE 1 — BUILDER (installs deps, compiles native modules, builds app) @@ -62,14 +65,35 @@ COPY . . # 4. Create directories that the app expects RUN mkdir -p database -# 5. Remove docs routes before build (avoids EXDEV rename error in overlayfs) -# and clean .svelte-kit so stale route types don't persist -RUN rm -rf src/routes/\(docs\) .svelte-kit +# 5. Conditionally remove docs routes before build +# (avoids EXDEV rename error in overlayfs; clean .svelte-kit so stale +# route types don't persist) +ARG WITH_DOCS +RUN if [ "$WITH_DOCS" != "true" ]; then \ + rm -rf src/routes/\(docs\) .svelte-kit; \ + fi # 6. Build: SvelteKit (vite) + server bundle (esbuild) -RUN npm run build +# Use build-with-docs when docs are enabled +RUN if [ "$WITH_DOCS" = "true" ]; then \ + npm run build-with-docs; \ + else \ + npm run build; \ + fi -# 7. Remove devDependencies from node_modules +# 7. Stage docs runtime files for index-docs (empty dir when docs disabled) +RUN mkdir -p /docs-runtime && \ + if [ "$WITH_DOCS" = "true" ]; then \ + mkdir -p /docs-runtime/scripts && \ + mkdir -p /docs-runtime/src/lib && \ + mkdir -p "/docs-runtime/src/routes/(docs)/docs" && \ + cp scripts/index-docs.ts /docs-runtime/scripts/ && \ + cp src/lib/marked.ts /docs-runtime/src/lib/ && \ + cp "src/routes/(docs)/docs.json" "/docs-runtime/src/routes/(docs)/" && \ + cp -r "src/routes/(docs)/docs/content" "/docs-runtime/src/routes/(docs)/docs/"; \ + fi + +# 8. Remove devDependencies from node_modules RUN npm prune --omit=dev # ============================================================================= @@ -137,6 +161,13 @@ COPY --chown=node:node --from=builder /app/src/lib/server/templates/general # Build output (SvelteKit client/server + esbuild main.js) — changes most often COPY --chown=node:node --from=builder /app/build ./build +# Docs runtime files (index-docs script + markdown sources; empty when WITH_DOCS=false) +COPY --chown=node:node --from=builder /docs-runtime/ ./ + +# Entrypoint script (runs index-docs on startup when docs are bundled) +COPY --chown=node:node docker-entrypoint.sh ./docker-entrypoint.sh +RUN chmod +x docker-entrypoint.sh + # ---- Runtime configuration ---- # Switch to non-root user @@ -148,5 +179,5 @@ EXPOSE ${PORT} HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD sh -c 'curl -sf http://localhost:${PORT}${KENER_BASE_PATH}/healthcheck || exit 1' -ENTRYPOINT ["node"] -CMD ["build/main.js"] +ENTRYPOINT ["./docker-entrypoint.sh"] +CMD ["node", "build/main.js"] diff --git a/README.md b/README.md index 9a3077ef..62613c99 100644 --- a/README.md +++ b/README.md @@ -78,14 +78,14 @@ git clone https://github.com/rajnandan1/kener.git cd kener # Uses docker-compose.yml (includes Redis + Kener) -# Set a strong KENER_SECRET_KEY in docker-compose.yml before first run +# Set a strong KENER_SECRET_KEY and ORIGIN in docker-compose.yml before first run docker compose up -d ``` Open `http://localhost:3000`. > [!IMPORTANT] -> Set a strong `KENER_SECRET_KEY` before starting for the first time. +> Set a strong `KENER_SECRET_KEY` and set `ORIGIN` to your public URL before starting for the first time. Use `docker-compose.dev.yml` when you want to build from local source instead of pulling the published image: @@ -113,6 +113,7 @@ docker run -d \ -p 3000:3000 \ -v "$(pwd)/database:/app/database" \ -e "KENER_SECRET_KEY=replace_with_a_random_string" \ + -e "ORIGIN=http://localhost:3000" \ -e "REDIS_URL=redis://host.docker.internal:6379" \ docker.io/rajnandan1/kener:latest ``` @@ -140,6 +141,7 @@ Create a `.env` with at least: ```dotenv KENER_SECRET_KEY=replace_with_a_random_string +ORIGIN=http://localhost:3000 REDIS_URL=redis://localhost:6379 PORT=3000 ``` diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index fc6327dd..18016ce6 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -36,20 +36,20 @@ services: args: VARIANT: alpine # or "debian" NODE_VERSION: 24 + WITH_DOCS: "true" container_name: kener-dev environment: KENER_SECRET_KEY: dev-secret-key-for-local-testing-only + ORIGIN: http://localhost:3000 REDIS_URL: redis://redis:6379 # DATABASE_URL: sqlite://./database/kener.sqlite.db ports: - "3000:3000" volumes: - dev_data:/app/database - - dev_uploads:/app/uploads depends_on: redis: condition: service_healthy volumes: dev_data: - dev_uploads: diff --git a/docker-compose.yml b/docker-compose.yml index 2a8b1f84..afd68203 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,7 @@ services: environment: # ── Required ── KENER_SECRET_KEY: replace_me_with_a_random_string # generate: openssl rand -base64 32 + ORIGIN: http://localhost:3000 # public URL of your Kener instance (required for CSRF protection) REDIS_URL: redis://redis:6379 # ── Database (default: SQLite) ── @@ -59,7 +60,6 @@ services: - "3000:3000" volumes: - data:/app/database - - uploads:/app/uploads depends_on: redis: condition: service_healthy @@ -107,8 +107,6 @@ services: volumes: data: name: kener_db - uploads: - name: kener_uploads redis_data: name: kener_redis # postgres_data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 00000000..5fe87ab4 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +# Index documentation into Redis when docs are bundled in the image +if [ -f /app/scripts/index-docs.ts ]; then + echo "[kener] Indexing documentation into Redis..." + node --experimental-strip-types /app/scripts/index-docs.ts || \ + echo "[kener] Warning: docs indexing failed (is REDIS_URL set?). Continuing..." +fi + +exec "$@" diff --git a/scripts/index-docs.ts b/scripts/index-docs.ts index c004f85c..6dc68439 100644 --- a/scripts/index-docs.ts +++ b/scripts/index-docs.ts @@ -16,7 +16,7 @@ import IORedis from "ioredis"; import dotenv from "dotenv"; import { marked } from "marked"; import plaintify from "marked-plaintify"; -import { mdToText } from "../src/lib/marked"; +import { mdToText } from "../src/lib/marked.ts"; dotenv.config(); diff --git a/scripts/main.ts b/scripts/main.ts index 95c20cf3..8a704b7e 100644 --- a/scripts/main.ts +++ b/scripts/main.ts @@ -1,5 +1,4 @@ import { handler } from "../build/handler.js"; -import { apiReference } from "@scalar/express-api-reference"; import dotenv from "dotenv"; dotenv.config(); import express from "express"; @@ -7,7 +6,6 @@ import Startup from "../src/lib/server/startup.ts"; import shutdownSchedulers from "../src/lib/server/schedulers/shutdown.ts"; import shutdownQueues from "../src/lib/server/queues/shutdown.ts"; import dbInstance from "../src/lib/server/db/db.ts"; -import fs from "fs-extra"; import knex from "knex"; import knexOb from "../knexfile.js"; diff --git a/src/routes/(docs)/docs/content/v4/getting-started/quick-start.md b/src/routes/(docs)/docs/content/v4/getting-started/quick-start.md index 1cc3bfc4..b47e5678 100644 --- a/src/routes/(docs)/docs/content/v4/getting-started/quick-start.md +++ b/src/routes/(docs)/docs/content/v4/getting-started/quick-start.md @@ -13,14 +13,14 @@ The fastest way to get started is with Docker Compose. git clone https://github.com/rajnandan1/kener.git cd kener -# Update KENER_SECRET_KEY in docker-compose.yml before first run +# Update KENER_SECRET_KEY and ORIGIN in docker-compose.yml before first run docker compose up -d ``` Kener will be available at `http://localhost:3000`. > [!IMPORTANT] -> Set a strong value for `KENER_SECRET_KEY` in `docker-compose.yml` before starting. +> Set a strong value for `KENER_SECRET_KEY` and set `ORIGIN` to your public URL in `docker-compose.yml` before starting. ### Run pre-built image {#run-pre-built-image-docker-hub-or-ghcr} @@ -57,6 +57,7 @@ Minimum `.env` for Docker: ```dotenv KENER_SECRET_KEY=replace_with_a_random_string +ORIGIN=http://localhost:3000 REDIS_URL=redis://host.docker.internal:6379 PORT=3000 ``` @@ -70,6 +71,7 @@ docker run -d \ -p 3000:3000 \ -v "$(pwd)/database:/app/database" \ -e "KENER_SECRET_KEY=replace_with_a_random_string" \ + -e "ORIGIN=http://localhost:3000" \ -e "REDIS_URL=redis://host.docker.internal:6379" \ docker.io/rajnandan1/kener:latest ``` @@ -122,6 +124,7 @@ Create or update your `.env`: ```dotenv KENER_SECRET_KEY=replace_with_a_random_string +ORIGIN=http://localhost:3000 REDIS_URL=redis://localhost:6379 PORT=3000 # Optional (defaults to SQLite): diff --git a/src/routes/(docs)/docs/content/v4/setup/environment-variables.md b/src/routes/(docs)/docs/content/v4/setup/environment-variables.md index d8856e5c..cd3641d6 100644 --- a/src/routes/(docs)/docs/content/v4/setup/environment-variables.md +++ b/src/routes/(docs)/docs/content/v4/setup/environment-variables.md @@ -44,6 +44,38 @@ node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" > [!WARNING] > Without `KENER_SECRET_KEY`, Kener will use a default key which is **not secure for production**. You'll see warnings in the console if running without this variable. +### ORIGIN {#origin} + +**Purpose**: The public-facing URL of your Kener instance. Required by SvelteKit for CSRF protection in production. + +**Why It's Required**: In production builds, SvelteKit validates that POST form submissions originate from the same site. Without `ORIGIN`, all form submissions (login, signup, settings, etc.) will fail with a **"Cross-site POST form submissions are forbidden"** error. + +**Requirements**: + +- Must include protocol (`http://` or `https://`) +- Must match the URL users access in their browser +- No trailing slash +- Not needed during local development (`vite dev` infers it automatically) + +**Examples**: + +```bash +# Local Docker testing +ORIGIN=http://localhost:3000 + +# Production +ORIGIN=https://status.example.com + +# With custom port +ORIGIN=https://status.example.com:8443 + +# With base path +ORIGIN=https://example.com +``` + +> [!CAUTION] +> Without `ORIGIN`, **all form submissions will be rejected** in production. This includes login, signup, and admin panel actions. Always set this variable when deploying. + ## Optional Variables {#optional-variables} ### KENER_BASE_PATH {#kener-base-path} diff --git a/src/routes/(manage)/manage/nav-documents.svelte b/src/routes/(manage)/manage/nav-documents.svelte deleted file mode 100644 index 0e81c2b8..00000000 --- a/src/routes/(manage)/manage/nav-documents.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - - - Documents - - {#each items as item (item.name)} - - - {#snippet child({ props })} - - - {item.name} - - {/snippet} - - - - {#snippet child({ props })} - - - More - - {/snippet} - - - - - Open - - - - Share - - - - - Delete - - - - - {/each} - - - - More - - - - diff --git a/src/routes/(manage)/manage/nav-main.svelte b/src/routes/(manage)/manage/nav-main.svelte index 96f06b68..b9d3f492 100644 --- a/src/routes/(manage)/manage/nav-main.svelte +++ b/src/routes/(manage)/manage/nav-main.svelte @@ -1,7 +1,4 @@ - - - - - {#each items as item (item.title)} - - - {#snippet child({ props })} - - - {item.title} - - {/snippet} - - - {/each} - - - diff --git a/src/routes/(manage)/manage/site-header.svelte b/src/routes/(manage)/manage/site-header.svelte index 82be0aeb..d3461c37 100644 --- a/src/routes/(manage)/manage/site-header.svelte +++ b/src/routes/(manage)/manage/site-header.svelte @@ -20,12 +20,14 @@ href={clientResolver(resolve, "/")} variant="secondary" size="sm" - class="dark:text-foreground hidden sm:flex" target="_blank" rel="noopener noreferrer" > Status Page +