Restructure repository into administration and website areas
@@ -3,5 +3,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
.offline-server.log
|
administration/.offline-server.log
|
||||||
.offline-server.pid
|
administration/.offline-server.pid
|
||||||
|
website/content/.editor-credentials.json
|
||||||
|
website/content/.editor-reset.json
|
||||||
|
website/content/.editor-rate-limit.json
|
||||||
|
|||||||
12
.gitignore
vendored
@@ -2,17 +2,17 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Local runtime files
|
# Local runtime files
|
||||||
.offline-server.log
|
administration/.offline-server.log
|
||||||
.offline-server.pid
|
administration/.offline-server.pid
|
||||||
|
|
||||||
# Editor auth + reset + rate limit state (never commit)
|
# Editor auth + reset + rate limit state (never commit)
|
||||||
content/.editor-credentials.json
|
website/content/.editor-credentials.json
|
||||||
content/.editor-reset.json
|
website/content/.editor-reset.json
|
||||||
content/.editor-rate-limit.json
|
website/content/.editor-rate-limit.json
|
||||||
|
|
||||||
# Generated backups
|
# Generated backups
|
||||||
*.bak
|
*.bak
|
||||||
content/*.bak
|
website/content/*.bak
|
||||||
|
|
||||||
# Optional local tooling
|
# Optional local tooling
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
91
README.md
@@ -1,87 +1,14 @@
|
|||||||
# ikfreunde WYSIWYG Multi-Route Deploy
|
# interkollektives micro website
|
||||||
|
|
||||||
This project runs a local-content WYSIWYG editor behind Traefik and supports multiple route instances on one domain.
|
Diese Repository-Struktur ist in zwei Zielgruppen getrennt:
|
||||||
|
|
||||||
Examples:
|
- `administration/`
|
||||||
- `https://mydomain.de/webpage1/`
|
Für Deployment, Docker, Traefik, Server-Skripte und technische Wartung.
|
||||||
- `https://mydomain.de/webpage2/`
|
|
||||||
- `https://mydomain.de/webpage3/`
|
|
||||||
|
|
||||||
## Files
|
- `website/`
|
||||||
- `docker-compose.traefik-routes.yml`: Traefik-ready multi-service compose file
|
Für Website-Inhalte (HTML, JSON, Bilder, Editor-Frontend) und redaktionelle Arbeit.
|
||||||
- `scripts/add-webpage.sh`: auto-generate new `webpageN` route + compose service
|
|
||||||
- `scripts/editor_server.php`: local API + static server (`/api/content`, `/api/save`)
|
|
||||||
- See [Brute-Force Protection](#brute-force-protection) for auth hardening details
|
|
||||||
|
|
||||||
## Requirements
|
## Einstieg
|
||||||
- Docker + Docker Compose
|
|
||||||
- Traefik with external network named `proxy`
|
|
||||||
|
|
||||||
## First deploy
|
- Technik/DevOps: siehe `administration/README.md`
|
||||||
```bash
|
- Redaktion/Content: siehe `website/README.md`
|
||||||
docker compose -f docker-compose.traefik-routes.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Add a new route (autogenerator)
|
|
||||||
```bash
|
|
||||||
./scripts/add-webpage.sh webpage4 mydomain.de
|
|
||||||
```
|
|
||||||
|
|
||||||
What it does:
|
|
||||||
1. Creates route data folder: `/srv/ikfreunde/webpage4/`
|
|
||||||
2. Seeds files if missing:
|
|
||||||
- `/srv/ikfreunde/webpage4/ikfreunde.com.html`
|
|
||||||
- `/srv/ikfreunde/webpage4/site-content.de.json`
|
|
||||||
3. Injects `webpage4` service into `docker-compose.traefik-routes.yml`
|
|
||||||
|
|
||||||
Then redeploy:
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose.traefik-routes.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
Open:
|
|
||||||
- `https://mydomain.de/webpage4/`
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- Edit mode is only active with `?edit=1`.
|
|
||||||
- Saves write both HTML and JSON and create `.bak` backups.
|
|
||||||
- Route names can include letters, numbers, `_`, `-`.
|
|
||||||
|
|
||||||
## Editor claim, login, reset (v1)
|
|
||||||
- New deployment starts as **unclaimed** (viewer-only by default).
|
|
||||||
- Open `https://mydomain.de/webpageN/?edit=1` to run first-time onboarding.
|
|
||||||
- First onboarding claim uses `email + password` and creates:
|
|
||||||
- `content/.editor-credentials.json`
|
|
||||||
- Afterwards, editing requires login. Without auth, users remain viewer.
|
|
||||||
|
|
||||||
### Password reset (without SMTP)
|
|
||||||
- On failed login, trigger reset request.
|
|
||||||
- Server writes reset data to:
|
|
||||||
- `content/.editor-reset.json`
|
|
||||||
- The file contains `reset_url` with token.
|
|
||||||
- Open that URL, set new password, then login again.
|
|
||||||
|
|
||||||
Security note:
|
|
||||||
- `content/.editor-credentials.json` and `content/.editor-reset.json` are blocked from HTTP access by the server router.
|
|
||||||
- Access to these files requires container/filesystem access.
|
|
||||||
- Simple brute-force protection is enabled in-app for login/reset (`content/.editor-rate-limit.json`) with account-based + global per-site thresholds (IP-independent).
|
|
||||||
- L3/L4 DDoS and global rate limiting should be handled at Traefik/network level.
|
|
||||||
|
|
||||||
## Brute-Force Protection
|
|
||||||
- Login/Reset limits are enforced in `scripts/editor_server.php`.
|
|
||||||
- Limiting is account-based + global per site (not IP-bound), so IP hopping is less effective.
|
|
||||||
- Buckets currently used:
|
|
||||||
- `login_account`, `login_global`
|
|
||||||
- `reset_request_account`, `reset_request_global`
|
|
||||||
- `reset_confirm_account`, `reset_confirm_global`
|
|
||||||
- Rate-limit state is stored in:
|
|
||||||
- `content/.editor-rate-limit.json`
|
|
||||||
|
|
||||||
## Optional env overrides
|
|
||||||
- `ROOT_BASE` (default: `/srv/ikfreunde`)
|
|
||||||
- `COMPOSE_FILE` (default: `docker-compose.traefik-routes.yml`)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```bash
|
|
||||||
ROOT_BASE=/data/pages COMPOSE_FILE=docker-compose.traefik-routes.yml ./scripts/add-webpage.sh webpage5 mydomain.de
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ COPY . /app
|
|||||||
|
|
||||||
EXPOSE 4173
|
EXPOSE 4173
|
||||||
|
|
||||||
CMD ["php", "-d", "opcache.enable_cli=0", "-S", "0.0.0.0:4173", "scripts/editor_server.php"]
|
CMD ["php", "-d", "opcache.enable_cli=0", "-S", "0.0.0.0:4173", "administration/scripts/editor_server.php"]
|
||||||
60
administration/README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Administration (Technik)
|
||||||
|
|
||||||
|
Dieser Bereich ist für Deployment, Betrieb und technische Wartung.
|
||||||
|
|
||||||
|
## Struktur
|
||||||
|
|
||||||
|
- `Dockerfile`
|
||||||
|
- `docker-compose.yml`
|
||||||
|
- `docker-compose.traefik-routes.yml`
|
||||||
|
- `scripts/` (Server, Extraktion, Route-Generator)
|
||||||
|
- `docs/` (Planungs-/Brainstorm-Dokumente)
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Docker + Docker Compose
|
||||||
|
- Traefik mit externem Netzwerk `proxy`
|
||||||
|
|
||||||
|
## Lokaler Editor-Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./administration/scripts/run_editor_server.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Aufruf: `http://127.0.0.1:4173/`
|
||||||
|
|
||||||
|
## Traefik Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f administration/docker-compose.traefik-routes.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Neue Route erzeugen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./administration/scripts/add-webpage.sh webpage4 mydomain.de
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f administration/docker-compose.traefik-routes.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security / Editor Auth
|
||||||
|
|
||||||
|
- Unclaimed by default (Viewer-Rolle)
|
||||||
|
- Claim/Login/Reset über API im `editor_server.php`
|
||||||
|
- Sensible Dateien liegen unter `website/content/` und sind via HTTP blockiert:
|
||||||
|
- `.editor-credentials.json`
|
||||||
|
- `.editor-reset.json`
|
||||||
|
- `.editor-rate-limit.json`
|
||||||
|
|
||||||
|
## Brute-Force Schutz
|
||||||
|
|
||||||
|
Buckets:
|
||||||
|
- `login_account`, `login_global`
|
||||||
|
- `reset_request_account`, `reset_request_global`
|
||||||
|
- `reset_confirm_account`, `reset_confirm_global`
|
||||||
|
|
||||||
|
Implementierung: `administration/scripts/editor_server.php`
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
services:
|
services:
|
||||||
webpage1:
|
webpage1:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: administration/Dockerfile
|
||||||
container_name: ikfreunde-webpage1
|
container_name: ikfreunde-webpage1
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/ikfreunde/webpage1/ikfreunde.com.html:/app/ikfreunde.com.html
|
- /srv/ikfreunde/webpage1/ikfreunde.com.html:/app/website/ikfreunde.com.html
|
||||||
- /srv/ikfreunde/webpage1/site-content.de.json:/app/content/site-content.de.json
|
- /srv/ikfreunde/webpage1/site-content.de.json:/app/website/content/site-content.de.json
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- proxy
|
||||||
@@ -24,12 +24,12 @@ services:
|
|||||||
|
|
||||||
webpage2:
|
webpage2:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: administration/Dockerfile
|
||||||
container_name: ikfreunde-webpage2
|
container_name: ikfreunde-webpage2
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/ikfreunde/webpage2/ikfreunde.com.html:/app/ikfreunde.com.html
|
- /srv/ikfreunde/webpage2/ikfreunde.com.html:/app/website/ikfreunde.com.html
|
||||||
- /srv/ikfreunde/webpage2/site-content.de.json:/app/content/site-content.de.json
|
- /srv/ikfreunde/webpage2/site-content.de.json:/app/website/content/site-content.de.json
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- proxy
|
||||||
@@ -47,12 +47,12 @@ services:
|
|||||||
|
|
||||||
webpage3:
|
webpage3:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: administration/Dockerfile
|
||||||
container_name: ikfreunde-webpage3
|
container_name: ikfreunde-webpage3
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/ikfreunde/webpage3/ikfreunde.com.html:/app/ikfreunde.com.html
|
- /srv/ikfreunde/webpage3/ikfreunde.com.html:/app/website/ikfreunde.com.html
|
||||||
- /srv/ikfreunde/webpage3/site-content.de.json:/app/content/site-content.de.json
|
- /srv/ikfreunde/webpage3/site-content.de.json:/app/website/content/site-content.de.json
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- proxy
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
services:
|
services:
|
||||||
editor:
|
editor:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: administration/Dockerfile
|
||||||
container_name: ikfreunde-editor
|
container_name: ikfreunde-editor
|
||||||
ports:
|
ports:
|
||||||
- "4173:4173"
|
- "4173:4173"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- ..:/app
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
command: ["php", "-d", "opcache.enable_cli=0", "-S", "0.0.0.0:4173", "scripts/editor_server.php"]
|
command: ["php", "-d", "opcache.enable_cli=0", "-S", "0.0.0.0:4173", "administration/scripts/editor_server.php"]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -16,8 +16,9 @@ if [[ ! "$NAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
ROOT_BASE="${ROOT_BASE:-/srv/ikfreunde}"
|
ROOT_BASE="${ROOT_BASE:-/srv/ikfreunde}"
|
||||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.traefik-routes.yml}"
|
COMPOSE_FILE="${COMPOSE_FILE:-$ROOT_DIR/administration/docker-compose.traefik-routes.yml}"
|
||||||
ROOT="${ROOT_BASE}/${NAME}"
|
ROOT="${ROOT_BASE}/${NAME}"
|
||||||
|
|
||||||
if [[ ! -f "$COMPOSE_FILE" ]]; then
|
if [[ ! -f "$COMPOSE_FILE" ]]; then
|
||||||
@@ -28,12 +29,12 @@ fi
|
|||||||
mkdir -p "$ROOT"
|
mkdir -p "$ROOT"
|
||||||
|
|
||||||
if [[ ! -f "$ROOT/ikfreunde.com.html" ]]; then
|
if [[ ! -f "$ROOT/ikfreunde.com.html" ]]; then
|
||||||
cp ikfreunde.com.html "$ROOT/ikfreunde.com.html"
|
cp "$ROOT_DIR/website/ikfreunde.com.html" "$ROOT/ikfreunde.com.html"
|
||||||
echo "Created: $ROOT/ikfreunde.com.html"
|
echo "Created: $ROOT/ikfreunde.com.html"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f "$ROOT/site-content.de.json" ]]; then
|
if [[ ! -f "$ROOT/site-content.de.json" ]]; then
|
||||||
cp content/site-content.de.json "$ROOT/site-content.de.json"
|
cp "$ROOT_DIR/website/content/site-content.de.json" "$ROOT/site-content.de.json"
|
||||||
echo "Created: $ROOT/site-content.de.json"
|
echo "Created: $ROOT/site-content.de.json"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -47,12 +48,12 @@ else
|
|||||||
|
|
||||||
${NAME}:
|
${NAME}:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: administration/Dockerfile
|
||||||
container_name: ikfreunde-${NAME}
|
container_name: ikfreunde-${NAME}
|
||||||
volumes:
|
volumes:
|
||||||
- ${ROOT}/ikfreunde.com.html:/app/ikfreunde.com.html
|
- ${ROOT}/ikfreunde.com.html:/app/website/ikfreunde.com.html
|
||||||
- ${ROOT}/site-content.de.json:/app/content/site-content.de.json
|
- ${ROOT}/site-content.de.json:/app/website/content/site-content.de.json
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- proxy
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
const PROJECT_ROOT = __DIR__ . '/..';
|
const PROJECT_ROOT = __DIR__ . '/../..';
|
||||||
const HTML_PATH = PROJECT_ROOT . '/ikfreunde.com.html';
|
const WEBSITE_ROOT = PROJECT_ROOT . '/website';
|
||||||
const JSON_PATH = PROJECT_ROOT . '/content/site-content.de.json';
|
const HTML_PATH = WEBSITE_ROOT . '/ikfreunde.com.html';
|
||||||
const CREDENTIALS_PATH = PROJECT_ROOT . '/content/.editor-credentials.json';
|
const JSON_PATH = WEBSITE_ROOT . '/content/site-content.de.json';
|
||||||
const RESET_PATH = PROJECT_ROOT . '/content/.editor-reset.json';
|
const CREDENTIALS_PATH = WEBSITE_ROOT . '/content/.editor-credentials.json';
|
||||||
const RATE_LIMIT_PATH = PROJECT_ROOT . '/content/.editor-rate-limit.json';
|
const RESET_PATH = WEBSITE_ROOT . '/content/.editor-reset.json';
|
||||||
|
const RATE_LIMIT_PATH = WEBSITE_ROOT . '/content/.editor-rate-limit.json';
|
||||||
const SESSION_TTL_SECONDS = 60 * 60 * 12; // 12h
|
const SESSION_TTL_SECONDS = 60 * 60 * 12; // 12h
|
||||||
const RESET_TTL_SECONDS = 60 * 30; // 30m
|
const RESET_TTL_SECONDS = 60 * 30; // 30m
|
||||||
const LOGIN_ACCOUNT_MAX_ATTEMPTS = 6;
|
const LOGIN_ACCOUNT_MAX_ATTEMPTS = 6;
|
||||||
@@ -970,8 +971,8 @@ function serveStatic(string $uri): void
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$resolved = realpath(PROJECT_ROOT . '/' . $cleanPath);
|
$resolved = realpath(WEBSITE_ROOT . '/' . $cleanPath);
|
||||||
$root = realpath(PROJECT_ROOT);
|
$root = realpath(WEBSITE_ROOT);
|
||||||
|
|
||||||
if ($resolved === false || $root === false || !str_starts_with($resolved, $root) || !is_file($resolved)) {
|
if ($resolved === false || $root === false || !str_starts_with($resolved, $root) || !is_file($resolved)) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
8
administration/scripts/extract_content.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INPUT_HTML="${1:-website/ikfreunde.com.html}"
|
||||||
|
OUTPUT_JSON="${2:-website/content/site-content.de.json}"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$OUTPUT_JSON")"
|
||||||
|
php -d opcache.enable_cli=0 administration/scripts/extract_dom_content.php "$INPUT_HTML" "$OUTPUT_JSON"
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
if ($argc < 3) {
|
if ($argc < 3) {
|
||||||
fwrite(STDERR, "Usage: php scripts/extract_dom_content.php <input_html> <output_json>\n");
|
fwrite(STDERR, "Usage: php administration/scripts/extract_dom_content.php <input_html> <output_json>\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
10
administration/scripts/run_editor_server.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
PORT="${1:-4173}"
|
||||||
|
|
||||||
|
ln -sf ikfreunde.com.html website/index.html
|
||||||
|
php -d opcache.enable_cli=0 -S 127.0.0.1:"$PORT" administration/scripts/editor_server.php
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
cd "$ROOT_DIR"
|
WEB_DIR="$ROOT_DIR/website"
|
||||||
|
|
||||||
PORT="${1:-4173}"
|
PORT="${1:-4173}"
|
||||||
PID_FILE=".offline-server.pid"
|
PID_FILE="$ROOT_DIR/administration/.offline-server.pid"
|
||||||
LOG_FILE=".offline-server.log"
|
LOG_FILE="$ROOT_DIR/administration/.offline-server.log"
|
||||||
|
|
||||||
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||||
echo "Offline server already running on PID $(cat "$PID_FILE")."
|
echo "Offline server already running on PID $(cat "$PID_FILE")."
|
||||||
@@ -16,19 +15,19 @@ fi
|
|||||||
|
|
||||||
rm -f "$PID_FILE"
|
rm -f "$PID_FILE"
|
||||||
|
|
||||||
ln -sf ikfreunde.com.html index.html
|
ln -sf ikfreunde.com.html "$WEB_DIR/index.html"
|
||||||
nohup python3 -m http.server "$PORT" --bind 127.0.0.1 >"$LOG_FILE" 2>&1 &
|
nohup python3 -m http.server "$PORT" --bind 127.0.0.1 --directory "$WEB_DIR" >"$LOG_FILE" 2>&1 &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
echo "$SERVER_PID" > "$PID_FILE"
|
echo "$SERVER_PID" > "$PID_FILE"
|
||||||
|
|
||||||
sleep 0.3
|
sleep 0.3
|
||||||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
echo "Failed to start offline server on port $PORT."
|
echo "Failed to start offline server on port $PORT."
|
||||||
echo "Check log: $ROOT_DIR/$LOG_FILE"
|
echo "Check log: $LOG_FILE"
|
||||||
rm -f "$PID_FILE"
|
rm -f "$PID_FILE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Offline server started (PID $SERVER_PID)."
|
echo "Offline server started (PID $SERVER_PID)."
|
||||||
echo "Open: http://127.0.0.1:${PORT}/"
|
echo "Open: http://127.0.0.1:${PORT}/"
|
||||||
echo "Log: $ROOT_DIR/$LOG_FILE"
|
echo "Log: $LOG_FILE"
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
cd "$ROOT_DIR"
|
PID_FILE="$ROOT_DIR/administration/.offline-server.pid"
|
||||||
|
|
||||||
PID_FILE=".offline-server.pid"
|
|
||||||
|
|
||||||
if [[ ! -f "$PID_FILE" ]]; then
|
if [[ ! -f "$PID_FILE" ]]; then
|
||||||
echo "No PID file found. Server may already be stopped."
|
echo "No PID file found. Server may already be stopped."
|
||||||
@@ -1 +1 @@
|
|||||||
ikfreunde.com.html
|
website/ikfreunde.com.html
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
INPUT_HTML="${1:-ikfreunde.com.html}"
|
|
||||||
OUTPUT_JSON="${2:-content/site-content.de.json}"
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "$OUTPUT_JSON")"
|
|
||||||
php -d opcache.enable_cli=0 scripts/extract_dom_content.php "$INPUT_HTML" "$OUTPUT_JSON"
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
cd "$ROOT_DIR"
|
|
||||||
|
|
||||||
PORT="${1:-4173}"
|
|
||||||
|
|
||||||
ln -sf ikfreunde.com.html index.html
|
|
||||||
php -d opcache.enable_cli=0 -S 127.0.0.1:"$PORT" scripts/editor_server.php
|
|
||||||
33
website/README.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Website (Content)
|
||||||
|
|
||||||
|
Dieser Bereich ist für Menschen gedacht, die Inhalte bearbeiten möchten.
|
||||||
|
|
||||||
|
## Was liegt hier?
|
||||||
|
|
||||||
|
- `ikfreunde.com.html` (die Seite)
|
||||||
|
- `content/site-content.de.json` (Text- und Bilddaten)
|
||||||
|
- `ikfreunde.com_files/` (Assets)
|
||||||
|
- `editor/` (WYSIWYG im Browser)
|
||||||
|
|
||||||
|
## Editor nutzen
|
||||||
|
|
||||||
|
1. Seite öffnen: `https://<domain>/<route>/?edit=1`
|
||||||
|
2. Beim ersten Mal: Claim mit `E-Mail + Passwort`
|
||||||
|
3. Danach: Login erforderlich, sonst Viewer-Modus
|
||||||
|
|
||||||
|
## Bearbeiten
|
||||||
|
|
||||||
|
- Text: Doppelklick auf Text
|
||||||
|
- Bilder: Klick aufs Bild, dann URL/Alt im Overlay ändern
|
||||||
|
- Speichern: über Editor-Steuerung
|
||||||
|
|
||||||
|
## Passwort-Reset
|
||||||
|
|
||||||
|
- Bei Login-Fehlern kann ein Reset angefordert werden
|
||||||
|
- Reset-Link wird in `content/.editor-reset.json` abgelegt
|
||||||
|
- Mit dem `reset_url` Link neues Passwort setzen
|
||||||
|
|
||||||
|
## Wichtiger Hinweis
|
||||||
|
|
||||||
|
Nicht direkt in versteckten `.editor-*` Dateien arbeiten.
|
||||||
|
Diese Dateien gehören zum Auth-System.
|
||||||
@@ -197,7 +197,7 @@
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const wantsReset = window.confirm(
|
const wantsReset = window.confirm(
|
||||||
"Login fehlgeschlagen. Passwort-Reset anfordern (Token wird in content/.editor-reset.json erzeugt)?"
|
"Login fehlgeschlagen. Passwort-Reset anfordern (Token wird in website/content/.editor-reset.json erzeugt)?"
|
||||||
);
|
);
|
||||||
if (wantsReset) {
|
if (wantsReset) {
|
||||||
await triggerResetRequest(email);
|
await triggerResetRequest(email);
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
alert(
|
alert(
|
||||||
"Reset angefordert. Prüfe im Container die Datei content/.editor-reset.json und nutze den reset_url Link."
|
"Reset angefordert. Prüfe im Container die Datei website/content/.editor-reset.json und nutze den reset_url Link."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |