From a82d9fe01ffade6d826bcb7ae53b1b0853296f92 Mon Sep 17 00:00:00 2001 From: Andrew Gioia Date: Mon, 21 Jul 2025 13:51:39 -0400 Subject: [PATCH] Adds basic subscription handling, fixes timezone issues app wide, improves event saving and timezone issues there, updates user table and seeding, moves to email address as principal uri component and not ulid --- .env.example | 20 +- app/Http/Controllers/CalendarController.php | 15 +- app/Http/Controllers/DavController.php | 24 +- app/Http/Controllers/EventController.php | 59 +- .../Controllers/SubscriptionController.php | 67 + app/Models/Calendar.php | 13 +- app/Models/CalendarInstance.php | 11 +- app/Models/Event.php | 6 + app/Models/User.php | 8 + app/Policies/CalendarPolicy.php | 2 +- app/Services/Dav/LaravelSabreAuthBackend.php | 13 +- .../Dav/LaravelSabrePrincipalBackend.php | 2 +- curl.html | 3891 ++++++++++++++++- .../0001_01_01_000000_create_users_table.php | 2 +- database/seeders/DatabaseSeeder.php | 2 +- resources/views/calendars/show.blade.php | 2 +- resources/views/events/form.blade.php | 18 +- routes/web.php | 5 + 18 files changed, 4096 insertions(+), 64 deletions(-) create mode 100644 app/Http/Controllers/SubscriptionController.php 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']);