diff --git a/.env.example b/.env.example index 35db1dd..a636174 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,20 @@ -APP_NAME=Laravel +APP_NAME=Kithkin APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://localhost - APP_LOCALE=en APP_FALLBACK_LOCALE=en APP_FAKER_LOCALE=en_US +APP_TIMEZONE=America/New_York # custom APP_MAINTENANCE_DRIVER=file # APP_MAINTENANCE_STORE=database +ADMIN_EMAIL=admin@kithkin.dev +ADMIN_PASSWORD= +ADMIN_NAME="Kithkin Admin" + PHP_CLI_SERVER_WORKERS=4 BCRYPT_ROUNDS=12 @@ -20,12 +24,12 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite -# DB_HOST=127.0.0.1 -# DB_PORT=3306 -# DB_DATABASE=laravel -# DB_USERNAME=root -# DB_PASSWORD= +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=kithkin +DB_USERNAME=root +DB_PASSWORD= SESSION_DRIVER=database SESSION_LIFETIME=120 diff --git a/app/Http/Controllers/CalendarController.php b/app/Http/Controllers/CalendarController.php index 8514c8d..c3c792b 100644 --- a/app/Http/Controllers/CalendarController.php +++ b/app/Http/Controllers/CalendarController.php @@ -7,6 +7,7 @@ use Illuminate\Support\Str; use Illuminate\Support\Facades\DB; use App\Models\Calendar; use App\Models\CalendarMeta; +use App\Models\CalendarInstance; class CalendarController extends Controller { @@ -15,7 +16,7 @@ class CalendarController extends Controller */ public function index() { - $principal = 'principals/' . auth()->id(); + $principal = auth()->user()->principal_uri; $calendars = Calendar::query() ->select('calendars.*', 'ci.displayname as instance_displayname') // ← add @@ -54,7 +55,7 @@ class CalendarController extends Controller // update the calendar instance row $instance = CalendarInstance::create([ 'calendarid' => $calId, - 'principaluri' => 'principals/'.auth()->id(), + 'principaluri' => auth()->user()->principal_uri, 'uri' => Str::uuid(), 'displayname' => $data['name'], 'description' => $data['description'] ?? null, @@ -81,16 +82,14 @@ class CalendarController extends Controller $this->authorize('view', $calendar); $calendar->load([ - 'meta', - 'instances' => fn ($q) => - $q->where('principaluri', 'principals/'.auth()->id()), + 'meta', + 'instances' => fn ($q) => $q->where('principaluri', auth()->user()->principal_uri), ]); /* grab the single instance for convenience in the view */ $instance = $calendar->instances->first(); $caldavUrl = $instance?->caldavUrl(); // null-safe - /* events + meta, newest first */ $events = $calendar->events() ->with('meta') @@ -113,7 +112,7 @@ class CalendarController extends Controller $calendar->load([ 'meta', 'instances' => fn ($q) => - $q->where('principaluri', 'principals/'.auth()->id()), + $q->where('principaluri', auth()->user()->principal_uri), ]); $instance = $calendar->instances->first(); // may be null but shouldn’t @@ -137,7 +136,7 @@ class CalendarController extends Controller // update the instance row $calendar->instances() - ->where('principaluri', 'principals/'.auth()->id()) + ->where('principaluri', auth()->user()->principal_uri) ->update([ 'displayname' => $data['name'], 'description' => $data['description'] ?? '', diff --git a/app/Http/Controllers/DavController.php b/app/Http/Controllers/DavController.php index 01b5681..f275463 100644 --- a/app/Http/Controllers/DavController.php +++ b/app/Http/Controllers/DavController.php @@ -4,42 +4,50 @@ namespace App\Http\Controllers; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\DB; use App\Services\Dav\LaravelSabreAuthBackend; use App\Services\Dav\LaravelSabrePrincipalBackend; use Sabre\DAV; use Sabre\DAV\Auth\Plugin as AuthPlugin; use Sabre\DAVACL\Plugin as ACLPlugin; use Sabre\DAVACL\PrincipalCollection; +use Sabre\CalDAV\CalendarRoot; +use Sabre\CalDAV\Plugin as CalDavPlugin; +use Sabre\CalDAV\Backend\PDO as CalDAVPDO; +use Sabre\CardDAV\AddressBookRoot; class DavController extends Controller { public function handle() { + // debug + \Log::info('SabreDAV DavController'); + + // get raw pdo from laravel + $pdo = DB::connection()->getPdo(); // get raw PDO from Laravel + + // setup the backends $authBackend = new LaravelSabreAuthBackend(); $principalBackend = new LaravelSabrePrincipalBackend(); - \Log::info('SabreDAV DavController'); + $calendarBackend = new CalDAVPDO($pdo); $nodes = [ new PrincipalCollection($principalBackend), + new CalendarRoot($principalBackend, $calendarBackend) // Add your Calendars or Addressbooks here ]; $server = new DAV\Server($nodes); $server->setBaseUri('/dav/'); - $server->addPlugin(new AuthPlugin($authBackend, 'WebDAV')); + $server->addPlugin(new AuthPlugin($authBackend, 'Kithkin DAV')); $server->addPlugin(new ACLPlugin()); + $server->addPlugin(new CalDavPlugin()); $server->on('beforeMethod', function () { \Log::info('SabreDAV beforeMethod triggered'); }); - ob_start(); - $server->exec(); - - $status = $server->httpResponse->getStatus(); - $content = ob_get_contents(); - ob_end_clean(); $server->exec(); exit; } diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php index de5af7e..a4d2762 100644 --- a/app/Http/Controllers/EventController.php +++ b/app/Http/Controllers/EventController.php @@ -12,14 +12,32 @@ use App\Models\EventMeta; class EventController extends Controller { + /** + * create a new event page + */ public function create(Calendar $calendar) { $this->authorize('update', $calendar); - return view('events.form', ['calendar' => $calendar, 'event' => new Event]); + + $instance = $calendar->instanceForUser(); + $event = new Event; + $event->meta = (object) [ + 'title' => '', + 'description' => '', + 'location' => '', + 'start_at' => null, + 'end_at' => null, + 'all_day' => false, + 'category' => '', + ]; + $start = $event->start_at; + $end = $event->end_at; + + return view('events.form', compact('calendar', 'instance', 'event', 'start', 'end')); } /** - * insert VEVENT into Sabre’s calendarobjects + meta row + * insert vevent into sabre’s calendarobjects + meta row */ public function store(Request $req, Calendar $calendar) { @@ -37,8 +55,13 @@ class EventController extends Controller // prepare payload $uid = Str::uuid() . '@' . parse_url(config('app.url'), PHP_URL_HOST); - $start = new Carbon($data['start_at'], $calendar->timezone); - $end = new Carbon($data['end_at'], $calendar->timezone); + + // store events as UTC in the database; convert to calendar time in the view + $client_timezone = $calendar->timezone ?? 'UTC'; + $start = Carbon::createFromFormat('Y-m-d\TH:i', $data['start_at'], $client_timezone) + ->setTimezone('UTC'); + $end = Carbon::createFromFormat('Y-m-d\TH:i', $data['end_at'], $client_timezone) + ->setTimezone('UTC'); // prepare strings $description = $data['description'] ?? ''; @@ -46,7 +69,6 @@ class EventController extends Controller $description = str_replace("\n", '\\n', $description); $location = str_replace("\n", '\\n', $location); - /* build minimal iCalendar payload */ $ical = <<route('calendars.show', $calendar); } + /** + * show the event edit form + */ public function edit(Calendar $calendar, Event $event) { $this->authorize('update', $calendar); - return view('events.form', compact('calendar', 'event')); + + $instance = $calendar->instanceForUser(); + $timezone = $instance?->timezone ?? 'UTC'; + + $start = optional($event->meta?->start_at) + ?->timezone($timezone) + ?->format('Y-m-d\TH:i'); + + $end = optional($event->meta?->end_at) + ?->timezone($timezone) + ?->format('Y-m-d\TH:i'); + + return view('events.form', compact('calendar', 'instance', 'event', 'start', 'end')); } /** @@ -112,9 +149,9 @@ ICS; ]); // rebuild the icalendar payload - $tz = $calendar->timezone; - $start = new \Carbon\Carbon($data['start_at'], $tz); - $end = new \Carbon\Carbon($data['end_at'], $tz); + $calendar_timezone = $calendar->timezone ?? 'UTC'; + $start = Carbon::createFromFormat('Y-m-d\TH:i', $data['start_at'], $calendar_timezone)->setTimezone($calendar_timezone); + $end = Carbon::createFromFormat('Y-m-d\TH:i', $data['end_at'], $calendar_timezone)->setTimezone($calendar_timezone); // prepare strings $description = $data['description'] ?? ''; @@ -132,8 +169,8 @@ PRODID:-//Kithkin//Laravel CalDAV//EN BEGIN:VEVENT UID:$uid DTSTAMP:{$start->utc()->format('Ymd\\THis\\Z')} -DTSTART;TZID={$tz}:{$start->format('Ymd\\THis')} -DTEND;TZID={$tz}:{$end->format('Ymd\\THis')} +DTSTART;TZID={$calendar_timezone}:{$start->format('Ymd\\THis')} +DTEND;TZID={$calendar_timezone}:{$end->format('Ymd\\THis')} SUMMARY:{$data['title']} DESCRIPTION:$description LOCATION:$location diff --git a/app/Http/Controllers/SubscriptionController.php b/app/Http/Controllers/SubscriptionController.php new file mode 100644 index 0000000..b8cbdcf --- /dev/null +++ b/app/Http/Controllers/SubscriptionController.php @@ -0,0 +1,67 @@ +firstOrFail(); + + $calendar = $instance->calendar()->with(['events.meta'])->firstOrFail(); + $timezone = $instance->timezone ?? 'UTC'; + + $ical = $this->generateICalendarFeed($calendar->events, $timezone); + + return Response::make($ical, 200, [ + 'Content-Type' => 'text/calendar; charset=utf-8', + 'Content-Disposition' => 'inline; filename="' . $calendarUri . '.ics"', + ]); + } + + protected function generateICalendarFeed($events, string $tz): string + { + $output = []; + $output[] = 'BEGIN:VCALENDAR'; + $output[] = 'VERSION:2.0'; + $output[] = 'PRODID:-//Kithkin Calendar//EN'; + $output[] = 'CALSCALE:GREGORIAN'; + $output[] = 'METHOD:PUBLISH'; + + foreach ($events as $event) { + $meta = $event->meta; + + if (!$meta || !$meta->start_at || !$meta->end_at) { + continue; + } + + $start = Carbon::parse($meta->start_at)->timezone($tz)->format('Ymd\THis'); + $end = Carbon::parse($meta->end_at)->timezone($tz)->format('Ymd\THis'); + + $output[] = 'BEGIN:VEVENT'; + $output[] = 'UID:' . $event->uid; + $output[] = 'SUMMARY:' . $this->escape($meta->title ?? '(Untitled)'); + $output[] = 'DESCRIPTION:' . $this->escape($meta->description ?? ''); + $output[] = 'DTSTART;TZID=' . $tz . ':' . $start; + $output[] = 'DTEND;TZID=' . $tz . ':' . $end; + $output[] = 'DTSTAMP:' . Carbon::parse($event->lastmodified)->format('Ymd\THis\Z'); + if ($meta->location) { + $output[] = 'LOCATION:' . $this->escape($meta->location); + } + $output[] = 'END:VEVENT'; + } + + $output[] = 'END:VCALENDAR'; + + return implode("\r\n", $output); + } + + protected function escape(?string $text): string + { + return str_replace(['\\', ';', ',', "\n"], ['\\\\', '\;', '\,', '\n'], $text ?? ''); + } +} diff --git a/app/Models/Calendar.php b/app/Models/Calendar.php index af7b985..fc762b1 100644 --- a/app/Models/Calendar.php +++ b/app/Models/Calendar.php @@ -15,7 +15,6 @@ class Calendar extends Model protected $fillable = [ 'displayname', 'description', - 'timezone', ]; /* all event components (VEVENT, VTODO, …) */ @@ -30,9 +29,19 @@ class Calendar extends Model return $this->hasOne(CalendarMeta::class, 'calendar_id'); } - /* get instance */ + /* get instances */ public function instances() { return $this->hasMany(CalendarInstance::class, 'calendarid'); } + + /* get the primary? instance for a user */ + public function instanceForUser(?User $user = null) + { + $user = $user ?? auth()->user(); + + return $this->instances() + ->where('principaluri', 'principals/' . $user->email) + ->first(); + } } diff --git a/app/Models/CalendarInstance.php b/app/Models/CalendarInstance.php index 85c2586..c7e0464 100644 --- a/app/Models/CalendarInstance.php +++ b/app/Models/CalendarInstance.php @@ -9,6 +9,15 @@ class CalendarInstance extends Model { protected $table = 'calendarinstances'; public $timestamps = false; + protected $fillable = [ + 'calendarid', + 'principaluri', + 'uri', + 'displayname', + 'description', + 'calendarcolor', + 'timezone' + ]; public function calendar(): BelongsTo { @@ -20,7 +29,7 @@ class CalendarInstance extends Model // e.g. https://kithkin.lan/dav/calendars/1/48f888f3-c5c5-…/ return url( '/dav/calendars/' . - auth()->id() . '/' . + auth()->user()->email . '/' . $this->uri . '/' ); } diff --git a/app/Models/Event.php b/app/Models/Event.php index 97646ae..40f3e4c 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -24,6 +24,12 @@ class Event extends Model 'calendardata', ]; + /* casts */ + protected $casts = [ + 'start_at' => 'datetime', + 'end_at' => 'datetime', + ]; + /* owning calendar */ public function calendar(): BelongsTo { diff --git a/app/Models/User.php b/app/Models/User.php index 0e19759..7bd188d 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -61,4 +61,12 @@ class User extends Authenticatable { return $this->hasMany(Calendar::class); } + + /** + * get the current user's principal uri + */ + public function getPrincipalUriAttribute(): string + { + return 'principals/' . $this->email; + } } diff --git a/app/Policies/CalendarPolicy.php b/app/Policies/CalendarPolicy.php index b3802eb..ba8d552 100644 --- a/app/Policies/CalendarPolicy.php +++ b/app/Policies/CalendarPolicy.php @@ -22,7 +22,7 @@ class CalendarPolicy public function view(User $user, Calendar $calendar): bool { return $calendar->instances() - ->where('principaluri', 'principals/'.$user->id) + ->where('principaluri', 'principals/'.$user->email) ->exists(); } diff --git a/app/Services/Dav/LaravelSabreAuthBackend.php b/app/Services/Dav/LaravelSabreAuthBackend.php index b35b410..1bbe27e 100644 --- a/app/Services/Dav/LaravelSabreAuthBackend.php +++ b/app/Services/Dav/LaravelSabreAuthBackend.php @@ -7,6 +7,8 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Log; use Sabre\DAV\Auth\Backend\AbstractBasic; use Sabre\DAV\Auth\Backend\BasicCallBack; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; class LaravelSabreAuthBackend extends AbstractBasic { @@ -15,6 +17,11 @@ class LaravelSabreAuthBackend extends AbstractBasic \Log::info('LaravelSabreAuthBackend instantiated'); } + public function challenge(RequestInterface $request, ResponseInterface $response) + { + $response->addHeader('WWW-Authenticate', 'Basic realm="WebDAV", charset="UTF-8"'); + } + protected function validateUserPass($username, $password) { $user = User::where('email', $username)->first(); @@ -23,18 +30,18 @@ class LaravelSabreAuthBackend extends AbstractBasic // THIS is the new required step $this->currentPrincipal = 'principals/' . $user->email; - \Log::info('SabreDAV auth success', ['principal' => $this->currentPrincipal]); + \Log::info('validateUserPass auth success', ['principal' => $this->currentPrincipal, 'username' => $username]); return true; } - \Log::warning('SabreDAV auth failed', ['username' => $username]); + \Log::warning('validateUserPass auth failed', ['username' => $username]); return false; } public function getCurrentPrincipal() { - \Log::debug('SabreDAV getCurrentPrincipal', ['current' => $$this->currentPrincipal]); + \Log::debug('getCurrentPrincipal', ['current' => $$this->currentPrincipal]); return $this->currentPrincipal; } } diff --git a/app/Services/Dav/LaravelSabrePrincipalBackend.php b/app/Services/Dav/LaravelSabrePrincipalBackend.php index fc613b9..b11fe61 100644 --- a/app/Services/Dav/LaravelSabrePrincipalBackend.php +++ b/app/Services/Dav/LaravelSabrePrincipalBackend.php @@ -13,7 +13,7 @@ class LaravelSabrePrincipalBackend extends AbstractBackend { return User::all()->map(function ($user) { return [ - 'uri' => 'principals/' . $user->id, + 'uri' => 'principals/' . $user->email, '{DAV:}displayname' => $user->name, ]; })->toArray(); diff --git a/curl.html b/curl.html index 0e70e41..8776507 100644 --- a/curl.html +++ b/curl.html @@ -1,11 +1,3886 @@ -HTTP/2 207 +HTTP/2 500 server: nginx/1.27.5 -date: Fri, 18 Jul 2025 13:51:55 GMT -content-type: application/xml; charset=utf-8 +content-type: text/html; charset=UTF-8 x-powered-by: PHP/8.4.8 -x-sabre-version: 4.7.0 -vary: Brief,Prefer -dav: 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search +cache-control: no-cache, private +date: Fri, 18 Jul 2025 18:35:47 GMT +set-cookie: kithkin_session=eyJpdiI6IlRralFpaGgxQWVrYktPaFV3YUdrZlE9PSIsInZhbHVlIjoic055b2ZzTTJGSytjTHBrcXZlTFhIRmZ3c2dTNzhTS0x0dWJweE1JbTdsSHNjdGNsUGdOeWs1K1ZJZ2htSExwY2hiZUk1Q1hhOXJ3L2cwOXdjb1lsa0p4ZklRWk0wWmVVOUVjUFNIMHUvV3ZLVElocW9YUFpDOHA4VHF4Q1kwSEgiLCJtYWMiOiIyZTJmZTAwMDk3ZTZmOWYyYzc5ZWNmOGFlNDU4NDZjMTQ3YzgwZWI1Njk5OGZkMjk2MGNkZDA1OGI0NTIzY2E3IiwidGFnIjoiIn0%3D; expires=Fri, 18 Jul 2025 20:35:47 GMT; Max-Age=7200; path=/; secure; httponly; samesite=lax - -/dav/principals/andrew@kithkin.lan/HTTP/1.1 200 OK + + + + + + + Kithkin + + + + + + + + + + +
+
+
+
+
+
+ + + +
+ + + Internal Server Error + +
+ +
+ + +
+ + + +
+
+
+
+
+ +
+
+
+
+
+
+ + + Error + +
+
+ Call to undefined method Sabre\DAV\Auth\Plugin::getRealm() +
+
+ + +
+
+ +
+
+
+ +
+
+
+
+ + app/Http/Controllers/DavController.php + + :51 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php + + :46 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Routing/Route.php + + :265 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Routing/Route.php + + :211 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Routing/Router.php + + :808 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :169 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php + + :50 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.php + + :48 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php + + :120 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php + + :63 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php + + :36 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php + + :74 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :126 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Routing/Router.php + + :807 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Routing/Router.php + + :786 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Routing/Router.php + + :750 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Routing/Router.php + + :739 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php + + :200 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :169 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php + + :21 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php + + :31 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php + + :21 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php + + :51 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Http/Middleware/ValidatePostSize.php + + :27 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php + + :109 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php + + :48 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php + + :58 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php + + :22 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Http/Middleware/ValidatePathEncoding.php + + :26 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :208 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php + + :126 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php + + :175 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php + + :144 +
+
+
+
+
+
+
+
+
+
+
+ + vendor/laravel/framework/src/Illuminate/Foundation/Application.php + + :1219 +
+
+
+
+
+
+
+
+
+
+
+ + public/index.php + + :20 +
+
+
+
+
+
+
+
+
+
+
+ + /Users/andrew/.composer/vendor/laravel/valet/server.php + + :110 +
+
+
+
+
+
+
+
+
+
+ +
+
+ Request +
+ +
+ PROPFIND + /dav/principals/andrew@kithkin.lan +
+ +
+ Headers +
+ +
+
+ + depth + + +
0
+
+
+
+ + accept + + +
*/*
+
+
+
+ + user-agent + + +
curl/8.7.1
+
+
+
+ + host + + +
kithkin.lan
+
+
+
+ +
+ Body +
+ +
+
+ +
No body data
+
+
+
+
+ +
+
+ Application +
+ +
+ Routing +
+ +
+
+ controller + +
App\Http\Controllers\DavController@handle
+
+
+
+ middleware + +
web
+
+
+
+ +
+ Routing Parameters +
+ +
+
+ +
{
+    "any": "principals/andrew@kithkin.lan"
+}
+
+
+
+ +
+ Database Queries + + +
+ +
+
+
+ mysql + +
+ +
select * from `sessions` where `id` = 'CKOo0isfBlZMqX4zJbt54MEIGhUE6B1J3V4LxW4c' limit 1
+
+
+
+
+
+
+
+ + + + + + diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 811be65..3ff7250 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -10,7 +10,7 @@ return new class extends Migration { Schema::create('users', function (Blueprint $table) { $table->ulid('id')->primary(); // ulid primary key - $table->string('uri')->unique()->nullable()->after('email'); // formerly from sabre principals table + $table->string('uri')->nullable(); // formerly from sabre principals table $table->string('displayname')->nullable(); // formerly from sabre principals table $table->string('name')->nullable(); // custom name if necessary $table->string('email')->unique(); diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 227109b..1785dad 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -27,7 +27,7 @@ class DatabaseSeeder extends Seeder ] ); - /** fill the Sabre-friendly columns */ + /** fill the sabre-friendly columns */ $user->update([ 'uri' => 'principals/'.$user->email, 'displayname' => $user->name, diff --git a/resources/views/calendars/show.blade.php b/resources/views/calendars/show.blade.php index 2fb2743..99ede30 100644 --- a/resources/views/calendars/show.blade.php +++ b/resources/views/calendars/show.blade.php @@ -32,7 +32,7 @@
{{ __('Timezone') }}
-
{{ $instance->timezone }}
+
{{ $instance?->timezone ?? __('Not set') }}
@php diff --git a/resources/views/events/form.blade.php b/resources/views/events/form.blade.php index 6c0c132..7cda452 100644 --- a/resources/views/events/form.blade.php +++ b/resources/views/events/form.blade.php @@ -30,7 +30,7 @@
+ :value="old('title', $event->meta?->title ?? '')" required autofocus />
@@ -38,7 +38,7 @@
+ class="mt-1 block w-full rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring">{{ old('description', $event->meta?->description ?? '') }}
@@ -46,7 +46,7 @@
+ :value="old('location', $event->meta?->location ?? '')" />
@@ -57,9 +57,8 @@ + :value="old('start_at', $start)" + required /> @@ -67,9 +66,8 @@ + :value="old('end_at', $end)" + required /> @@ -78,7 +76,7 @@ {{-- All-day --}}
meta->all_day)) /> + @checked(old('all_day', $event->meta?->all_day)) /> diff --git a/routes/web.php b/routes/web.php index 8b937ae..49dbfa8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,7 @@ use App\Http\Controllers\ProfileController; use App\Http\Controllers\CalendarController; use App\Http\Controllers\EventController; use App\Http\Controllers\DavController; +use App\Http\Controllers\SubscriptionController; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; /* @@ -56,9 +57,13 @@ require __DIR__.'/auth.php'; | authentication for DAV clients. */ +// default dav handling Route::match( ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'REPORT', 'LOCK', 'UNLOCK'], '/dav/{any?}', [DavController::class, 'handle'] )->where('any', '.*') ->withoutMiddleware([VerifyCsrfToken::class]); + +// subscriptions +Route::get('/subscriptions/{calendarUri}.ics', [SubscriptionController::class, 'download']);