🎉(all) bootstrap the Calendars project

This repository was forked from Drive in late December 2025 and
boostraped as a minimal demo of backend+caldav server+frontend
integration. There is much left to do and to fix!
This commit is contained in:
Sylvain Zimmer
2026-01-09 00:51:25 +01:00
commit a36348ead1
298 changed files with 41036 additions and 0 deletions

2358
docker/auth/realm.json Normal file

File diff suppressed because it is too large Load Diff

39
docker/davical/Dockerfile Normal file
View File

@@ -0,0 +1,39 @@
# DAViCal CalDAV Server
# Based on Debian with Apache and PHP
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
# Install dependencies
RUN apt-get update && apt-get install -y \
apache2 \
libapache2-mod-php \
php-pgsql \
php-xml \
php-curl \
php-imap \
php-ldap \
davical \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Enable required Apache modules
RUN a2enmod rewrite
# Copy Apache configuration
COPY davical.conf /etc/apache2/sites-available/davical.conf
RUN a2dissite 000-default && a2ensite davical
# Copy DAViCal configuration
COPY config.php /etc/davical/config.php
# Copy and setup entrypoint
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Set permissions
RUN chown -R www-data:www-data /var/log/apache2
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]

64
docker/davical/config.php Normal file
View File

@@ -0,0 +1,64 @@
<?php
/**
* DAViCal Configuration
* This file is mounted as /etc/davical/config.php
* Overrides the default config generated by the fintechstudios/davical image
*/
// Database connection - uses shared calendars database in public schema
// The image will set these from PGHOST, PGDATABASE, PGUSER, PGPASSWORD
$c->pg_connect[] = 'host=' . getenv('PGHOST') . ' port=' . (getenv('PGPORT') ?: '5432') . ' dbname=' . getenv('PGDATABASE') . ' user=' . getenv('PGUSER') . ' password=' . getenv('PGPASSWORD');
// System name
$c->system_name = 'Calendars DAViCal Server';
// Admin email
$c->admin_email = 'admin@example.com';
// Allow public access for CalDAV discovery
$c->public_freebusy_url = true;
// Default locale
$c->default_locale = 'en_US.UTF-8';
// Logging - enable for debugging authentication issues
$c->log_caldav_queries = true;
// Trust proxy headers for auth
$c->trust_x_forwarded = true;
// Configure base path when behind reverse proxy
// Override SCRIPT_NAME so DAViCal generates correct URLs
// DAViCal uses $_SERVER['SCRIPT_NAME'] to determine the base path for URLs
// We set it to the proxy path WITHOUT /caldav.php since DAViCal will add that itself
if (isset($_SERVER['HTTP_X_FORWARDED_PREFIX'])) {
$_SERVER['SCRIPT_NAME'] = rtrim($_SERVER['HTTP_X_FORWARDED_PREFIX'], '/');
} elseif (isset($_SERVER['HTTP_X_SCRIPT_NAME'])) {
$_SERVER['SCRIPT_NAME'] = rtrim($_SERVER['HTTP_X_SCRIPT_NAME'], '/');
}
// Custom authentication function to use X-Forwarded-User header
// This function is called by DAViCal's authentication system
function authenticate_via_forwarded_user( $username, $password ) {
// Check if X-Forwarded-User header is present
if (isset($_SERVER['HTTP_X_FORWARDED_USER'])) {
$forwarded_user = trim($_SERVER['HTTP_X_FORWARDED_USER']);
// If the username from Basic Auth matches X-Forwarded-User, authenticate
// Users with password '*' are externally authenticated
if (strtolower($username) === strtolower($forwarded_user)) {
// Return the username to authenticate as this user
// DAViCal will check if user exists and has password '*'
return $forwarded_user;
}
}
// Fall back to standard authentication
return false;
}
// Use custom authentication hook
$c->authenticate_hook = array(
'call' => 'authenticate_via_forwarded_user',
'config' => array()
);

View File

@@ -0,0 +1,22 @@
<VirtualHost *:80>
ServerName localhost
DocumentRoot /usr/share/davical/htdocs
DirectoryIndex index.php
Alias /images/ /usr/share/davical/htdocs/images/
<Directory /usr/share/davical/htdocs>
AllowOverride All
Require all granted
</Directory>
AcceptPathInfo On
# CalDAV principal URL
RewriteEngine On
RewriteRule ^/caldav/(.*)$ /caldav.php/$1 [L]
RewriteRule ^/\.well-known/caldav /caldav.php [R=301,L]
ErrorLog ${APACHE_LOG_DIR}/davical_error.log
CustomLog ${APACHE_LOG_DIR}/davical_access.log combined
</VirtualHost>

93
docker/davical/run-migrations Executable file
View File

@@ -0,0 +1,93 @@
#!/bin/bash
###
# Run DB migrations necessary to use Davical.
# Will create the database on first-run, and only run necessary migrations on subsequent runs.
#
# Requires the following environment variables in addition to the container variables.
# - ROOT_PGUSER
# - ROOT_PGPASSWORD
# - DAVICAL_ADMIN_PASS
###
set -e
if [ -z ${ROOT_PGUSER+x} ]; then
echo "ROOT_PGUSER must be set"
exit 1
fi
if [ -z ${ROOT_PGPASSWORD+x} ]; then
echo "ROOT_PGPASSWORD must be set"
exit 1
fi
if [ -z ${DAVICAL_ADMIN_PASS+x} ]; then
echo "DAVICAL_ADMIN_PASS must be set"
exit 1
fi
if [ -z ${DBA_PGPASSWORD+x} ]; then
DBA_PGPASSWORD=$PGPASSWORD
fi
if [ -z ${DAVICAL_SCHEMA+x} ]; then
DAVICAL_SCHEMA=$DBA_PGUSER
fi
# store PG environment so it can be overridden as-needed
DAVICAL_PGUSER=$PGUSER
DAVICAL_PGPASSWORD=$PGPASSWORD
DAVICAL_PGDATABASE=$PGDATABASE
run_migrations() {
echo "Running dba/update-davical-database, which should automatically apply any necessary DB migrations."
/usr/share/davical/dba/update-davical-database \
--dbname $DAVICAL_PGDATABASE \
--dbuser $DBA_PGUSER \
--dbhost $PGHOST \
--dbpass $DBA_PGPASSWORD \
--appuser $DAVICAL_PGUSER \
--owner $DBA_PGUSER
}
export PGUSER=$ROOT_PGUSER
export PGPASSWORD=$ROOT_PGPASSWORD
export PGDATABASE=
# Wait for PG connection
retries=10
until pg_isready -q -t 3; do
[[ retries -eq 0 ]] && echo "Could not connect to Postgres" && exit 1
echo "Waiting for Postgres to be available"
retries=$((retries-1))
sleep 1
done
# Check whether the database has already been setup, with awl tables.
tables=$(psql -d $DAVICAL_PGDATABASE -c "\\dt")
if echo "$tables" | grep -q "awl_db_revision"; then
# The database already exists - just run any outstanding migrations
run_migrations
exit 0
fi
echo "Database has not been created - running first-time database setup"
# the rest of the commands are run as the dba superuser
export PGUSER=$DBA_PGUSER
export PGPASSWORD=$DBA_PGPASSWORD
export PGDATABASE=$DAVICAL_PGDATABASE
psql -qXAt -f /usr/share/awl/dba/awl-tables.sql
psql -qXAt -f /usr/share/awl/dba/schema-management.sql
psql -qXAt -f /usr/share/davical/dba/davical.sql
run_migrations
psql -qXAt -f /usr/share/davical/dba/base-data.sql
# DAViCal only uses salted SHA1 at-best, but it's better than storing the password in plaintext!
# see https://wiki.davical.org/index.php?title=Force_Admin_Password
# from https://gitlab.com/davical-project/awl/-/blob/3f044e2dc8435c2eeba61a3c41ec11c820711ab3/inc/DataUpdate.php#L48-58
salted_password=$(php -r 'require "/usr/share/awl/inc/AWLUtilities.php"; echo session_salted_sha1($argv[1]);' "$DAVICAL_ADMIN_PASS")
psql -qX \
-v pw="'$salted_password'" \
<<EOF
UPDATE usr SET password = :pw WHERE user_no = 1;
EOF

View File

@@ -0,0 +1,28 @@
server {
listen 8083;
server_name localhost;
charset utf-8;
# API routes - proxy to Django backend
location /api/ {
proxy_pass http://backend-dev:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Frontend - proxy to Next.js dev server
location / {
proxy_pass http://frontend-dev:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support for HMR
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

View File

@@ -0,0 +1,46 @@
#!/bin/bash
# Initialize multiple databases on the same PostgreSQL server
# This script runs automatically when the PostgreSQL container starts for the first time
set -e
set -u
# Function to create a database and user if they don't exist
create_database() {
local database=$1
local user=$2
local password=$3
echo "Creating database '$database' with user '$user'..."
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
-- Create user if not exists
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '$user') THEN
CREATE USER $user WITH PASSWORD '$password';
END IF;
END
\$\$;
-- Create database if not exists
SELECT 'CREATE DATABASE $database OWNER $user'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$database')\gexec
-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE $database TO $user;
EOSQL
echo "Database '$database' created successfully."
}
# Create databases for all services
# The main 'calendar' database is created by default via POSTGRES_DB
# DAViCal database
create_database "davical" "davical" "davical_pass"
# Keycloak database
create_database "keycloak" "keycloak" "keycloak_pass"
echo "All databases initialized successfully!"

View File

@@ -0,0 +1,15 @@
server {
listen 8923;
server_name localhost;
charset utf-8;
# Keycloak - all auth-related paths
location / {
proxy_pass http://keycloak:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,36 @@
#!/bin/sh
# Initialize shared calendars database for local development
# All services (Django, DAViCal, Keycloak) use the same database in public schema
# This script runs as POSTGRES_USER on first database initialization
set -e
echo "Initializing calendars database..."
# Ensure pgroot user exists with correct password and permissions
# This runs as POSTGRES_USER (which may be different from pgroot on existing databases)
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
-- Ensure pgroot user exists with correct password
-- POSTGRES_USER has superuser privileges, so it can create users
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'pgroot') THEN
CREATE USER pgroot WITH PASSWORD 'pass' SUPERUSER CREATEDB CREATEROLE;
ELSE
ALTER USER pgroot WITH PASSWORD 'pass';
-- Ensure superuser privileges
ALTER USER pgroot WITH SUPERUSER CREATEDB CREATEROLE;
END IF;
END
\$\$;
-- Grant all privileges on calendars database
GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO pgroot;
-- Grant all on public schema
GRANT ALL ON SCHEMA public TO pgroot;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO pgroot;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO pgroot;
EOSQL
echo "Calendars database ready!"