Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# ============================================================================
# Dungeon Master's Vault — Docker Environment Configuration
#
# Copy this file to .env and update the values:
# cp .env.example .env
#
# Or run the setup script to generate .env with secure random values:
# ./docker-setup.sh
# ============================================================================

# --- Application ---
PORT=8890

# --- Datomic Database ---
# ADMIN_PASSWORD secures the Datomic admin interface
# DATOMIC_PASSWORD is used by the application to connect to Datomic
# The password in DATOMIC_URL must match DATOMIC_PASSWORD
ADMIN_PASSWORD=change-me-admin
DATOMIC_PASSWORD=change-me-datomic
DATOMIC_URL=datomic:free://datomic:4334/orcpub?password=change-me-datomic

# --- Security ---
# Secret used to sign JWT tokens (20+ characters recommended)
SIGNATURE=change-me-to-something-unique-and-long

# --- Email (SMTP) ---
# Leave EMAIL_SERVER_URL empty to disable email functionality
EMAIL_SERVER_URL=
EMAIL_ACCESS_KEY=
EMAIL_SECRET_KEY=
EMAIL_SERVER_PORT=587
EMAIL_FROM_ADDRESS=
EMAIL_ERRORS_TO=
EMAIL_SSL=FALSE
EMAIL_TLS=FALSE

# --- Initial Admin User (optional) ---
# Set these then run: ./docker-user.sh init
# Safe to run multiple times — duplicates are skipped.
INIT_ADMIN_USER=
INIT_ADMIN_EMAIL=
INIT_ADMIN_PASSWORD=
303 changes: 303 additions & 0 deletions .github/workflows/docker-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
name: Docker Integration Test

on:
pull_request:
branches: [develop]
paths:
- 'docker/**'
- 'docker-compose*.yaml'
- 'docker-setup.sh'
- 'docker-user.sh'
- 'deploy/**'
- '.github/workflows/docker-integration.yml'
workflow_dispatch:

jobs:
docker-test:
name: Docker Setup & User Management
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Lint shell scripts
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
shellcheck docker-setup.sh docker-user.sh

- name: Run docker-setup.sh --auto
run: |
./docker-setup.sh --auto
echo "--- Generated .env (secrets redacted) ---"
sed 's/=.*/=***/' .env

- name: Validate .env password consistency
run: |
PW=$(grep '^DATOMIC_PASSWORD=' .env | cut -d= -f2)
URL_PW=$(grep '^DATOMIC_URL=' .env | sed 's/.*password=//')
if [ "$PW" != "$URL_PW" ]; then
echo "FAIL: DATOMIC_PASSWORD and DATOMIC_URL password mismatch"
exit 1
fi
echo "OK: Passwords match"

- name: Test — setup --force preserves existing values
run: |
# Save original passwords
ORIG_ADMIN=$(grep '^ADMIN_PASSWORD=' .env | cut -d= -f2)
ORIG_DATOMIC=$(grep '^DATOMIC_PASSWORD=' .env | cut -d= -f2)
ORIG_SIG=$(grep '^SIGNATURE=' .env | cut -d= -f2)

# Re-run with --force --auto (should regenerate)
./docker-setup.sh --auto --force

# Verify .env was regenerated (new passwords, since --auto generates fresh ones)
NEW_ADMIN=$(grep '^ADMIN_PASSWORD=' .env | cut -d= -f2)
NEW_DATOMIC=$(grep '^DATOMIC_PASSWORD=' .env | cut -d= -f2)

# Verify structure is intact
grep -q '^DATOMIC_URL=' .env || { echo "FAIL: DATOMIC_URL missing"; exit 1; }
grep -q '^SIGNATURE=' .env || { echo "FAIL: SIGNATURE missing"; exit 1; }
grep -q '^PORT=' .env || { echo "FAIL: PORT missing"; exit 1; }

# Re-check password consistency after --force
PW=$(grep '^DATOMIC_PASSWORD=' .env | cut -d= -f2)
URL_PW=$(grep '^DATOMIC_URL=' .env | sed 's/.*password=//')
if [ "$PW" != "$URL_PW" ]; then
echo "FAIL: Password mismatch after --force re-run"
exit 1
fi
echo "OK: --force regenerated .env with consistent passwords"

- name: Start datomic (no deps)
run: |
docker compose pull
docker compose up -d --no-deps datomic
echo "Datomic container started, waiting for health..."

- name: Wait for datomic healthy
run: |
for i in $(seq 1 90); do
CID=$(docker compose ps -q datomic 2>/dev/null) || true
if [ -z "$CID" ]; then
echo " [$i/90] datomic container not found yet"
sleep 2
continue
fi
RUNNING=$(docker inspect --format='{{.State.Running}}' "$CID" 2>/dev/null || echo "false")
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$CID" 2>/dev/null || echo "starting")
echo " [$i/90] running=$RUNNING health=$STATUS"
if [ "$STATUS" = "healthy" ]; then
echo "Datomic is healthy (after ~$((i * 2))s)"
break
fi
if [ "$RUNNING" = "false" ]; then
echo "WARN: datomic container stopped — dumping logs"
docker compose logs datomic
echo "Container will restart (restart: always), continuing to wait..."
fi
if [ "$i" -eq 90 ]; then
echo "FAIL: Datomic did not become healthy within 180s"
echo "=== container state ==="
docker inspect --format='{{json .State}}' "$CID" | python3 -m json.tool || true
echo "=== datomic logs ==="
docker compose logs datomic
exit 1
fi
sleep 2
done

- name: Start orcpub and web
run: |
docker compose up -d
docker compose ps

- name: Wait for orcpub healthy
run: |
for i in $(seq 1 90); do
CID=$(docker compose ps -q orcpub 2>/dev/null) || true
if [ -z "$CID" ]; then
echo " [$i/90] orcpub container not found yet"
sleep 2
continue
fi
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$CID" 2>/dev/null || echo "starting")
echo " [$i/90] orcpub health=$STATUS"
if [ "$STATUS" = "healthy" ]; then
echo "orcpub is healthy (after ~$((i * 2))s)"
break
fi
if [ "$STATUS" = "unhealthy" ]; then
echo "FAIL: orcpub reported unhealthy"
echo "=== all logs ==="
docker compose logs
exit 1
fi
if [ "$i" -eq 90 ]; then
echo "FAIL: orcpub did not become healthy within 180s"
docker compose logs
exit 1
fi
sleep 2
done
docker compose ps

- name: Test — create user
run: |
./docker-user.sh create testadmin admin@test.local SecurePass123
echo "Exit code: $?"

- name: Test — check user exists
run: |
OUTPUT=$(./docker-user.sh check testadmin)
echo "$OUTPUT"
echo "$OUTPUT" | grep -q "testadmin"
echo "$OUTPUT" | grep -q "admin@test.local"
echo "$OUTPUT" | grep -q "true" # verified

- name: Test — list includes user
run: |
OUTPUT=$(./docker-user.sh list)
echo "$OUTPUT"
echo "$OUTPUT" | grep -q "testadmin"

- name: Test — duplicate user fails
run: |
if ./docker-user.sh create testadmin admin@test.local SecurePass123 2>&1; then
echo "FAIL: Should have rejected duplicate user"
exit 1
fi
echo "OK: Duplicate user correctly rejected"

- name: Test — create second user
run: ./docker-user.sh create player2 player2@test.local AnotherPass456

- name: Test — list shows both users
run: |
OUTPUT=$(./docker-user.sh list)
echo "$OUTPUT"
echo "$OUTPUT" | grep -q "testadmin"
echo "$OUTPUT" | grep -q "player2"

- name: Test — verify already-verified user is idempotent
run: |
OUTPUT=$(./docker-user.sh verify testadmin)
echo "$OUTPUT"
echo "$OUTPUT" | grep -q "already verified"

- name: Test — batch create users (with duplicates)
run: |
cat > /tmp/test-users.txt <<'TXT'
# Test batch file
batch1 batch1@test.local BatchPass111
batch2 batch2@test.local BatchPass222
# This next line is a duplicate from earlier single-create test
testadmin admin@test.local SecurePass123
TXT
OUTPUT=$(./docker-user.sh batch /tmp/test-users.txt)
echo "$OUTPUT"
echo "$OUTPUT" | grep -q "batch1"
echo "$OUTPUT" | grep -q "batch2"
echo "$OUTPUT" | grep -q "SKIP.*testadmin"
echo "$OUTPUT" | grep -q "2 created"
echo "$OUTPUT" | grep -q "1 skipped (duplicate)"
echo "$OUTPUT" | grep -q "0 failed"
echo "OK: Batch created 2 new, skipped 1 duplicate"

- name: Test — batch users appear in list
run: |
OUTPUT=$(./docker-user.sh list)
echo "$OUTPUT"
echo "$OUTPUT" | grep -q "batch1"
echo "$OUTPUT" | grep -q "batch2"

- name: Test — init creates admin from .env
run: |
# Append INIT_ADMIN_* vars to .env
printf '\nINIT_ADMIN_USER=initadmin\nINIT_ADMIN_EMAIL=initadmin@test.local\nINIT_ADMIN_PASSWORD=InitPass789\n' >> .env

# Run init
OUTPUT=$(./docker-user.sh init)
echo "$OUTPUT"
echo "$OUTPUT" | grep -q "initadmin"

# Verify user was created
CHECK=$(./docker-user.sh check initadmin)
echo "$CHECK"
echo "$CHECK" | grep -q "initadmin"
echo "$CHECK" | grep -q "initadmin@test.local"
echo "$CHECK" | grep -q "true"
echo "OK: init created admin from .env"

- name: Test — init is idempotent (re-run skips existing)
run: |
# Running init again should not fail — duplicate is handled
if ./docker-user.sh init 2>&1; then
echo "FAIL: init should exit non-zero for duplicate user"
exit 1
fi
echo "OK: init correctly reports duplicate on re-run"

- name: Test — check nonexistent user fails
run: |
if ./docker-user.sh check nobody@nowhere.com 2>&1; then
echo "FAIL: Should have reported user not found"
exit 1
fi
echo "OK: Nonexistent user correctly not found"

- name: Test — created user can log in via HTTP
run: |
# Use nginx (port 443) since orcpub:8890 is not exposed to host
RESPONSE=$(curl -sk -X POST https://localhost/login \
-H "Content-Type: application/json" \
-d '{"username":"testadmin","password":"SecurePass123"}' \
-w "\n%{http_code}" 2>&1) || true

HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')

echo "HTTP $HTTP_CODE"
echo "$BODY"

if [ "$HTTP_CODE" = "200" ]; then
echo "OK: Login succeeded"
echo "$BODY" | grep -q "token"
echo "OK: Response contains JWT token"
else
echo "FAIL: Expected HTTP 200, got $HTTP_CODE"
exit 1
fi

- name: Test — wrong password is rejected
run: |
HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" \
-X POST https://localhost/login \
-H "Content-Type: application/json" \
-d '{"username":"testadmin","password":"WrongPassword"}' 2>&1) || true

echo "HTTP $HTTP_CODE"
if [ "$HTTP_CODE" = "401" ]; then
echo "OK: Wrong password correctly rejected"
else
echo "FAIL: Expected HTTP 401, got $HTTP_CODE"
exit 1
fi

- name: Collect logs on failure
if: failure()
run: |
echo "=== docker compose ps ==="
docker compose ps
echo "=== datomic logs ==="
docker compose logs datomic
echo "=== orcpub logs ==="
docker compose logs orcpub
echo "=== web logs ==="
docker compose logs web

- name: Cleanup
if: always()
run: docker compose down -v
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pom.xml
orcpub.iml
profiles.clj
env.sh
.env
.repl
.nrepl-port
*~
Expand All @@ -34,3 +35,6 @@ deploy/homebrew/*

# As created by some LSP-protocol tooling, e.g. nvim-lsp
.lsp

# Claude Code local session data
.claude-data/
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,25 @@ You will need a few tools:
`git clone https://github.com/Orcpub/orcpub.git` if you don't have a github account

`git clone git@github.com:Orcpub/orcpub.git` if you do want to make changes to the code and make pull requests.


### Quick Setup (Recommended)

Run the automated setup script to generate secure passwords, SSL certificates, and all required directories:

```bash
./docker-setup.sh # Interactive — prompts for each value
./docker-setup.sh --auto # Non-interactive — generates secure defaults
```

Then start the containers and create your first user:

```bash
docker-compose up -d
./docker-user.sh create admin admin@example.com MySecurePass123
```

The `create` command creates a **pre-verified** account — no SMTP server or email confirmation needed. For batch user creation, additional commands, and full details see the [Docker User Management](docs/docker-user-management.md) guide.

### Edit docker-compose.yaml

Edit the `docker-compose.yaml` and update all the environmental variables and or paths as needed.
Expand Down
Loading
Loading