Initial commit. Working login, example UI with prototype and demo, connections to DB and DAV, local and prod setups.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "example/sabredav-server",
|
||||
"description": "CardDAV and CalDAV server using SabreDAV",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"sabre/dav": "^4.6"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
/**
|
||||
* Save as: ./sabredav/server.php
|
||||
*
|
||||
* SabreDAV CardDAV and CalDAV server implementation with PostgreSQL
|
||||
*/
|
||||
|
||||
use Sabre\DAV;
|
||||
use Sabre\CalDAV;
|
||||
use Sabre\CardDAV;
|
||||
use Sabre\DAVACL;
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
// PostgreSQL connection
|
||||
$dbHost = getenv('POSTGRES_HOST') ?: 'postgres';
|
||||
$dbPort = getenv('POSTGRES_PORT') ?: '5432';
|
||||
$dbName = getenv('POSTGRES_DB') ?: 'mydb';
|
||||
$dbUser = getenv('POSTGRES_USER') ?: 'myuser';
|
||||
$dbPass = getenv('POSTGRES_PASSWORD') ?: 'mypassword';
|
||||
|
||||
$dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $dbHost, $dbPort, $dbName);
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, $dbUser, $dbPass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database connection failed: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
die("Database connection failed");
|
||||
}
|
||||
|
||||
// Create tables if they don't exist
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS dav_principals (
|
||||
id SERIAL PRIMARY KEY,
|
||||
uri VARCHAR(255) NOT NULL UNIQUE,
|
||||
email VARCHAR(255),
|
||||
displayname VARCHAR(255)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dav_calendars (
|
||||
id SERIAL PRIMARY KEY,
|
||||
principaluri VARCHAR(255) NOT NULL,
|
||||
displayname VARCHAR(255),
|
||||
uri VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
calendarorder INT DEFAULT 0,
|
||||
calendarcolor VARCHAR(10),
|
||||
timezone TEXT,
|
||||
components VARCHAR(255),
|
||||
transparent BOOLEAN DEFAULT FALSE,
|
||||
UNIQUE(principaluri, uri)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dav_calendarobjects (
|
||||
id SERIAL PRIMARY KEY,
|
||||
calendardata TEXT,
|
||||
uri VARCHAR(255) NOT NULL,
|
||||
calendarid INTEGER NOT NULL REFERENCES dav_calendars(id) ON DELETE CASCADE,
|
||||
lastmodified INTEGER,
|
||||
etag VARCHAR(32),
|
||||
size INTEGER,
|
||||
componenttype VARCHAR(8),
|
||||
firstoccurence INTEGER,
|
||||
lastoccurence INTEGER,
|
||||
uid VARCHAR(255),
|
||||
UNIQUE(calendarid, uri)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dav_addressbooks (
|
||||
id SERIAL PRIMARY KEY,
|
||||
principaluri VARCHAR(255) NOT NULL,
|
||||
displayname VARCHAR(255),
|
||||
uri VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
synctoken INTEGER DEFAULT 1,
|
||||
UNIQUE(principaluri, uri)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dav_cards (
|
||||
id SERIAL PRIMARY KEY,
|
||||
addressbookid INTEGER NOT NULL REFERENCES dav_addressbooks(id) ON DELETE CASCADE,
|
||||
carddata TEXT,
|
||||
uri VARCHAR(255) NOT NULL,
|
||||
lastmodified INTEGER,
|
||||
etag VARCHAR(32),
|
||||
size INTEGER,
|
||||
UNIQUE(addressbookid, uri)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dav_addressbookchanges (
|
||||
id SERIAL PRIMARY KEY,
|
||||
uri VARCHAR(255) NOT NULL,
|
||||
synctoken INTEGER NOT NULL,
|
||||
addressbookid INTEGER NOT NULL REFERENCES dav_addressbooks(id) ON DELETE CASCADE,
|
||||
operation SMALLINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dav_calendarchanges (
|
||||
id SERIAL PRIMARY KEY,
|
||||
uri VARCHAR(255) NOT NULL,
|
||||
synctoken INTEGER NOT NULL,
|
||||
calendarid INTEGER NOT NULL REFERENCES dav_calendars(id) ON DELETE CASCADE,
|
||||
operation SMALLINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dav_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
digesta1 VARCHAR(32) NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_calendarobjects_calendarid ON dav_calendarobjects(calendarid);
|
||||
CREATE INDEX IF NOT EXISTS idx_cards_addressbookid ON dav_cards(addressbookid);
|
||||
");
|
||||
|
||||
// Create default principal if not exists
|
||||
$stmt = $pdo->query("SELECT COUNT(*) FROM dav_principals");
|
||||
if ($stmt->fetchColumn() == 0) {
|
||||
$pdo->exec("
|
||||
INSERT INTO dav_principals (uri, email, displayname)
|
||||
VALUES ('principals/default', 'admin@example.com', 'Default User')
|
||||
");
|
||||
}
|
||||
|
||||
// Create default addressbook if not exists
|
||||
$stmt = $pdo->query("SELECT COUNT(*) FROM dav_addressbooks");
|
||||
if ($stmt->fetchColumn() == 0) {
|
||||
$pdo->exec("
|
||||
INSERT INTO dav_addressbooks (principaluri, displayname, uri, description, synctoken)
|
||||
VALUES ('principals/default', 'Contacts', 'default', 'Default address book', 1)
|
||||
");
|
||||
}
|
||||
|
||||
// Create default calendar if not exists
|
||||
$stmt = $pdo->query("SELECT COUNT(*) FROM dav_calendars");
|
||||
if ($stmt->fetchColumn() == 0) {
|
||||
$pdo->exec("
|
||||
INSERT INTO dav_calendars (principaluri, displayname, uri, description, components, transparent)
|
||||
VALUES ('principals/default', 'Default Calendar', 'default', 'Default calendar', 'VEVENT,VTODO', false)
|
||||
");
|
||||
}
|
||||
|
||||
// Create default user if not exists (username: admin, password: admin)
|
||||
$stmt = $pdo->query("SELECT COUNT(*) FROM dav_users");
|
||||
if ($stmt->fetchColumn() == 0) {
|
||||
// MD5 hash of "admin:SabreDAV:admin"
|
||||
$digest = md5('admin:SabreDAV:admin');
|
||||
$pdo->exec("
|
||||
INSERT INTO dav_users (username, digesta1)
|
||||
VALUES ('admin', '$digest')
|
||||
");
|
||||
}
|
||||
|
||||
// Custom auth backend for PostgreSQL
|
||||
class PostgreSQLAuthBackend extends DAV\Auth\Backend\AbstractDigest {
|
||||
protected $pdo;
|
||||
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
$this->realm = 'SabreDAV';
|
||||
}
|
||||
|
||||
public function getDigestHash($realm, $username) {
|
||||
$stmt = $this->pdo->prepare('SELECT digesta1 FROM dav_users WHERE username = ?');
|
||||
$stmt->execute([$username]);
|
||||
return $stmt->fetchColumn() ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom principal backend for PostgreSQL
|
||||
class PostgreSQLPrincipalBackend extends DAVACL\PrincipalBackend\AbstractBackend {
|
||||
protected $pdo;
|
||||
protected $tableName = 'dav_principals';
|
||||
|
||||
public function __construct(PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
public function getPrincipalsByPrefix($prefixPath) {
|
||||
$stmt = $this->pdo->query('SELECT uri, email, displayname FROM ' . $this->tableName);
|
||||
$principals = [];
|
||||
|
||||
while ($row = $stmt->fetch()) {
|
||||
if (strpos($row['uri'], $prefixPath) !== 0) continue;
|
||||
|
||||
$principals[] = [
|
||||
'uri' => $row['uri'],
|
||||
'{DAV:}displayname' => $row['displayname'],
|
||||
'{http://sabredav.org/ns}email-address' => $row['email'],
|
||||
];
|
||||
}
|
||||
return $principals;
|
||||
}
|
||||
|
||||
public function getPrincipalByPath($path) {
|
||||
$stmt = $this->pdo->prepare('SELECT uri, email, displayname FROM ' . $this->tableName . ' WHERE uri = ?');
|
||||
$stmt->execute([$path]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row) return null;
|
||||
|
||||
return [
|
||||
'uri' => $row['uri'],
|
||||
'{DAV:}displayname' => $row['displayname'],
|
||||
'{http://sabredav.org/ns}email-address' => $row['email'],
|
||||
];
|
||||
}
|
||||
|
||||
public function updatePrincipal($path, $mutations) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getGroupMemberSet($principal) {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getGroupMembership($principal) {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function setGroupMemberSet($principal, array $members) {
|
||||
throw new DAV\Exception('Setting group members is not supported');
|
||||
}
|
||||
}
|
||||
|
||||
// Backends
|
||||
$authBackend = new PostgreSQLAuthBackend($pdo);
|
||||
$principalBackend = new PostgreSQLPrincipalBackend($pdo);
|
||||
|
||||
// Use SabreDAV's PostgreSQL backends for CardDAV and CalDAV
|
||||
$carddavBackend = new CardDAV\Backend\PDO($pdo);
|
||||
$carddavBackend->tableName = 'dav_cards';
|
||||
$carddavBackend->addressBooksTableName = 'dav_addressbooks';
|
||||
$carddavBackend->addressBookChangesTableName = 'dav_addressbookchanges';
|
||||
|
||||
$caldavBackend = new CalDAV\Backend\PDO($pdo);
|
||||
$caldavBackend->tableName = 'dav_calendarobjects';
|
||||
$caldavBackend->calendarTableName = 'dav_calendars';
|
||||
$caldavBackend->calendarChangesTableName = 'dav_calendarchanges';
|
||||
|
||||
// Directory structure
|
||||
$tree = [
|
||||
new DAVACL\PrincipalCollection($principalBackend),
|
||||
new CalDAV\CalendarRoot($principalBackend, $caldavBackend),
|
||||
new CardDAV\AddressBookRoot($principalBackend, $carddavBackend),
|
||||
];
|
||||
|
||||
$server = new DAV\Server($tree);
|
||||
$server->setBaseUri('/dav/');
|
||||
|
||||
// Plugins
|
||||
$server->addPlugin(new DAV\Auth\Plugin($authBackend));
|
||||
$server->addPlugin(new DAVACL\Plugin());
|
||||
$server->addPlugin(new DAV\Browser\Plugin());
|
||||
$server->addPlugin(new CalDAV\Plugin());
|
||||
$server->addPlugin(new CardDAV\Plugin());
|
||||
$server->addPlugin(new CalDAV\Schedule\Plugin());
|
||||
$server->addPlugin(new DAV\Sync\Plugin());
|
||||
|
||||
// Run the server
|
||||
$server->exec();
|
||||
Reference in New Issue
Block a user