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

This commit is contained in:
Andrew Gioia 2025-07-21 13:51:39 -04:00
parent 8d0738019f
commit a82d9fe01f
Signed by: andrew
GPG Key ID: FC09694A000800C8
18 changed files with 4096 additions and 64 deletions

View File

@ -1,16 +1,20 @@
APP_NAME=Laravel APP_NAME=Kithkin
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost APP_URL=http://localhost
APP_LOCALE=en APP_LOCALE=en
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US APP_FAKER_LOCALE=en_US
APP_TIMEZONE=America/New_York # custom
APP_MAINTENANCE_DRIVER=file APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database # APP_MAINTENANCE_STORE=database
ADMIN_EMAIL=admin@kithkin.dev
ADMIN_PASSWORD=
ADMIN_NAME="Kithkin Admin"
PHP_CLI_SERVER_WORKERS=4 PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12 BCRYPT_ROUNDS=12
@ -20,12 +24,12 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=sqlite DB_CONNECTION=mysql
# DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
# DB_PORT=3306 DB_PORT=3306
# DB_DATABASE=laravel DB_DATABASE=kithkin
# DB_USERNAME=root DB_USERNAME=root
# DB_PASSWORD= DB_PASSWORD=
SESSION_DRIVER=database SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120

View File

@ -7,6 +7,7 @@ use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use App\Models\Calendar; use App\Models\Calendar;
use App\Models\CalendarMeta; use App\Models\CalendarMeta;
use App\Models\CalendarInstance;
class CalendarController extends Controller class CalendarController extends Controller
{ {
@ -15,7 +16,7 @@ class CalendarController extends Controller
*/ */
public function index() public function index()
{ {
$principal = 'principals/' . auth()->id(); $principal = auth()->user()->principal_uri;
$calendars = Calendar::query() $calendars = Calendar::query()
->select('calendars.*', 'ci.displayname as instance_displayname') // ← add ->select('calendars.*', 'ci.displayname as instance_displayname') // ← add
@ -54,7 +55,7 @@ class CalendarController extends Controller
// update the calendar instance row // update the calendar instance row
$instance = CalendarInstance::create([ $instance = CalendarInstance::create([
'calendarid' => $calId, 'calendarid' => $calId,
'principaluri' => 'principals/'.auth()->id(), 'principaluri' => auth()->user()->principal_uri,
'uri' => Str::uuid(), 'uri' => Str::uuid(),
'displayname' => $data['name'], 'displayname' => $data['name'],
'description' => $data['description'] ?? null, 'description' => $data['description'] ?? null,
@ -81,16 +82,14 @@ class CalendarController extends Controller
$this->authorize('view', $calendar); $this->authorize('view', $calendar);
$calendar->load([ $calendar->load([
'meta', 'meta',
'instances' => fn ($q) => 'instances' => fn ($q) => $q->where('principaluri', auth()->user()->principal_uri),
$q->where('principaluri', 'principals/'.auth()->id()),
]); ]);
/* grab the single instance for convenience in the view */ /* grab the single instance for convenience in the view */
$instance = $calendar->instances->first(); $instance = $calendar->instances->first();
$caldavUrl = $instance?->caldavUrl(); // null-safe $caldavUrl = $instance?->caldavUrl(); // null-safe
/* events + meta, newest first */ /* events + meta, newest first */
$events = $calendar->events() $events = $calendar->events()
->with('meta') ->with('meta')
@ -113,7 +112,7 @@ class CalendarController extends Controller
$calendar->load([ $calendar->load([
'meta', 'meta',
'instances' => fn ($q) => 'instances' => fn ($q) =>
$q->where('principaluri', 'principals/'.auth()->id()), $q->where('principaluri', auth()->user()->principal_uri),
]); ]);
$instance = $calendar->instances->first(); // may be null but shouldnt $instance = $calendar->instances->first(); // may be null but shouldnt
@ -137,7 +136,7 @@ class CalendarController extends Controller
// update the instance row // update the instance row
$calendar->instances() $calendar->instances()
->where('principaluri', 'principals/'.auth()->id()) ->where('principaluri', auth()->user()->principal_uri)
->update([ ->update([
'displayname' => $data['name'], 'displayname' => $data['name'],
'description' => $data['description'] ?? '', 'description' => $data['description'] ?? '',

View File

@ -4,42 +4,50 @@ namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use App\Services\Dav\LaravelSabreAuthBackend; use App\Services\Dav\LaravelSabreAuthBackend;
use App\Services\Dav\LaravelSabrePrincipalBackend; use App\Services\Dav\LaravelSabrePrincipalBackend;
use Sabre\DAV; use Sabre\DAV;
use Sabre\DAV\Auth\Plugin as AuthPlugin; use Sabre\DAV\Auth\Plugin as AuthPlugin;
use Sabre\DAVACL\Plugin as ACLPlugin; use Sabre\DAVACL\Plugin as ACLPlugin;
use Sabre\DAVACL\PrincipalCollection; 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 class DavController extends Controller
{ {
public function handle() 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(); $authBackend = new LaravelSabreAuthBackend();
$principalBackend = new LaravelSabrePrincipalBackend(); $principalBackend = new LaravelSabrePrincipalBackend();
\Log::info('SabreDAV DavController'); $calendarBackend = new CalDAVPDO($pdo);
$nodes = [ $nodes = [
new PrincipalCollection($principalBackend), new PrincipalCollection($principalBackend),
new CalendarRoot($principalBackend, $calendarBackend)
// Add your Calendars or Addressbooks here // Add your Calendars or Addressbooks here
]; ];
$server = new DAV\Server($nodes); $server = new DAV\Server($nodes);
$server->setBaseUri('/dav/'); $server->setBaseUri('/dav/');
$server->addPlugin(new AuthPlugin($authBackend, 'WebDAV')); $server->addPlugin(new AuthPlugin($authBackend, 'Kithkin DAV'));
$server->addPlugin(new ACLPlugin()); $server->addPlugin(new ACLPlugin());
$server->addPlugin(new CalDavPlugin());
$server->on('beforeMethod', function () { $server->on('beforeMethod', function () {
\Log::info('SabreDAV beforeMethod triggered'); \Log::info('SabreDAV beforeMethod triggered');
}); });
ob_start();
$server->exec();
$status = $server->httpResponse->getStatus();
$content = ob_get_contents();
ob_end_clean();
$server->exec(); $server->exec();
exit; exit;
} }

View File

@ -12,14 +12,32 @@ use App\Models\EventMeta;
class EventController extends Controller class EventController extends Controller
{ {
/**
* create a new event page
*/
public function create(Calendar $calendar) public function create(Calendar $calendar)
{ {
$this->authorize('update', $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 Sabres calendarobjects + meta row * insert vevent into sabres calendarobjects + meta row
*/ */
public function store(Request $req, Calendar $calendar) public function store(Request $req, Calendar $calendar)
{ {
@ -37,8 +55,13 @@ class EventController extends Controller
// prepare payload // prepare payload
$uid = Str::uuid() . '@' . parse_url(config('app.url'), PHP_URL_HOST); $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 // prepare strings
$description = $data['description'] ?? ''; $description = $data['description'] ?? '';
@ -46,7 +69,6 @@ class EventController extends Controller
$description = str_replace("\n", '\\n', $description); $description = str_replace("\n", '\\n', $description);
$location = str_replace("\n", '\\n', $location); $location = str_replace("\n", '\\n', $location);
/* build minimal iCalendar payload */ /* build minimal iCalendar payload */
$ical = <<<ICS $ical = <<<ICS
BEGIN:VCALENDAR BEGIN:VCALENDAR
@ -89,10 +111,25 @@ ICS;
return redirect()->route('calendars.show', $calendar); return redirect()->route('calendars.show', $calendar);
} }
/**
* show the event edit form
*/
public function edit(Calendar $calendar, Event $event) public function edit(Calendar $calendar, Event $event)
{ {
$this->authorize('update', $calendar); $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 // rebuild the icalendar payload
$tz = $calendar->timezone; $calendar_timezone = $calendar->timezone ?? 'UTC';
$start = new \Carbon\Carbon($data['start_at'], $tz); $start = Carbon::createFromFormat('Y-m-d\TH:i', $data['start_at'], $calendar_timezone)->setTimezone($calendar_timezone);
$end = new \Carbon\Carbon($data['end_at'], $tz); $end = Carbon::createFromFormat('Y-m-d\TH:i', $data['end_at'], $calendar_timezone)->setTimezone($calendar_timezone);
// prepare strings // prepare strings
$description = $data['description'] ?? ''; $description = $data['description'] ?? '';
@ -132,8 +169,8 @@ PRODID:-//Kithkin//Laravel CalDAV//EN
BEGIN:VEVENT BEGIN:VEVENT
UID:$uid UID:$uid
DTSTAMP:{$start->utc()->format('Ymd\\THis\\Z')} DTSTAMP:{$start->utc()->format('Ymd\\THis\\Z')}
DTSTART;TZID={$tz}:{$start->format('Ymd\\THis')} DTSTART;TZID={$calendar_timezone}:{$start->format('Ymd\\THis')}
DTEND;TZID={$tz}:{$end->format('Ymd\\THis')} DTEND;TZID={$calendar_timezone}:{$end->format('Ymd\\THis')}
SUMMARY:{$data['title']} SUMMARY:{$data['title']}
DESCRIPTION:$description DESCRIPTION:$description
LOCATION:$location LOCATION:$location

View File

@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers;
use App\Models\CalendarInstance;
use Illuminate\Support\Facades\Response;
use Carbon\Carbon;
class SubscriptionController extends Controller
{
public function download(string $calendarUri)
{
$instance = CalendarInstance::where('uri', $calendarUri)->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 ?? '');
}
}

View File

@ -15,7 +15,6 @@ class Calendar extends Model
protected $fillable = [ protected $fillable = [
'displayname', 'displayname',
'description', 'description',
'timezone',
]; ];
/* all event components (VEVENT, VTODO, …) */ /* all event components (VEVENT, VTODO, …) */
@ -30,9 +29,19 @@ class Calendar extends Model
return $this->hasOne(CalendarMeta::class, 'calendar_id'); return $this->hasOne(CalendarMeta::class, 'calendar_id');
} }
/* get instance */ /* get instances */
public function instances() public function instances()
{ {
return $this->hasMany(CalendarInstance::class, 'calendarid'); 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();
}
} }

View File

@ -9,6 +9,15 @@ class CalendarInstance extends Model
{ {
protected $table = 'calendarinstances'; protected $table = 'calendarinstances';
public $timestamps = false; public $timestamps = false;
protected $fillable = [
'calendarid',
'principaluri',
'uri',
'displayname',
'description',
'calendarcolor',
'timezone'
];
public function calendar(): BelongsTo public function calendar(): BelongsTo
{ {
@ -20,7 +29,7 @@ class CalendarInstance extends Model
// e.g. https://kithkin.lan/dav/calendars/1/48f888f3-c5c5-…/ // e.g. https://kithkin.lan/dav/calendars/1/48f888f3-c5c5-…/
return url( return url(
'/dav/calendars/' . '/dav/calendars/' .
auth()->id() . '/' . auth()->user()->email . '/' .
$this->uri . '/' $this->uri . '/'
); );
} }

View File

@ -24,6 +24,12 @@ class Event extends Model
'calendardata', 'calendardata',
]; ];
/* casts */
protected $casts = [
'start_at' => 'datetime',
'end_at' => 'datetime',
];
/* owning calendar */ /* owning calendar */
public function calendar(): BelongsTo public function calendar(): BelongsTo
{ {

View File

@ -61,4 +61,12 @@ class User extends Authenticatable
{ {
return $this->hasMany(Calendar::class); return $this->hasMany(Calendar::class);
} }
/**
* get the current user's principal uri
*/
public function getPrincipalUriAttribute(): string
{
return 'principals/' . $this->email;
}
} }

View File

@ -22,7 +22,7 @@ class CalendarPolicy
public function view(User $user, Calendar $calendar): bool public function view(User $user, Calendar $calendar): bool
{ {
return $calendar->instances() return $calendar->instances()
->where('principaluri', 'principals/'.$user->id) ->where('principaluri', 'principals/'.$user->email)
->exists(); ->exists();
} }

View File

@ -7,6 +7,8 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Sabre\DAV\Auth\Backend\AbstractBasic; use Sabre\DAV\Auth\Backend\AbstractBasic;
use Sabre\DAV\Auth\Backend\BasicCallBack; use Sabre\DAV\Auth\Backend\BasicCallBack;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class LaravelSabreAuthBackend extends AbstractBasic class LaravelSabreAuthBackend extends AbstractBasic
{ {
@ -15,6 +17,11 @@ class LaravelSabreAuthBackend extends AbstractBasic
\Log::info('LaravelSabreAuthBackend instantiated'); \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) protected function validateUserPass($username, $password)
{ {
$user = User::where('email', $username)->first(); $user = User::where('email', $username)->first();
@ -23,18 +30,18 @@ class LaravelSabreAuthBackend extends AbstractBasic
// THIS is the new required step // THIS is the new required step
$this->currentPrincipal = 'principals/' . $user->email; $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; return true;
} }
\Log::warning('SabreDAV auth failed', ['username' => $username]); \Log::warning('validateUserPass auth failed', ['username' => $username]);
return false; return false;
} }
public function getCurrentPrincipal() public function getCurrentPrincipal()
{ {
\Log::debug('SabreDAV getCurrentPrincipal', ['current' => $$this->currentPrincipal]); \Log::debug('getCurrentPrincipal', ['current' => $$this->currentPrincipal]);
return $this->currentPrincipal; return $this->currentPrincipal;
} }
} }

View File

@ -13,7 +13,7 @@ class LaravelSabrePrincipalBackend extends AbstractBackend
{ {
return User::all()->map(function ($user) { return User::all()->map(function ($user) {
return [ return [
'uri' => 'principals/' . $user->id, 'uri' => 'principals/' . $user->email,
'{DAV:}displayname' => $user->name, '{DAV:}displayname' => $user->name,
]; ];
})->toArray(); })->toArray();

3891
curl.html

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@ return new class extends Migration
{ {
Schema::create('users', function (Blueprint $table) { Schema::create('users', function (Blueprint $table) {
$table->ulid('id')->primary(); // ulid primary key $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('displayname')->nullable(); // formerly from sabre principals table
$table->string('name')->nullable(); // custom name if necessary $table->string('name')->nullable(); // custom name if necessary
$table->string('email')->unique(); $table->string('email')->unique();

View File

@ -27,7 +27,7 @@ class DatabaseSeeder extends Seeder
] ]
); );
/** fill the Sabre-friendly columns */ /** fill the sabre-friendly columns */
$user->update([ $user->update([
'uri' => 'principals/'.$user->email, 'uri' => 'principals/'.$user->email,
'displayname' => $user->name, 'displayname' => $user->name,

View File

@ -32,7 +32,7 @@
<div> <div>
<dt class="text-sm font-medium text-gray-500">{{ __('Timezone') }}</dt> <dt class="text-sm font-medium text-gray-500">{{ __('Timezone') }}</dt>
<dd class="mt-1 text-gray-900">{{ $instance->timezone }}</dd> <dd class="mt-1 text-gray-900"> {{ $instance?->timezone ?? __('Not set') }}</dd>
</div> </div>
@php @php

View File

@ -30,7 +30,7 @@
<div class="mb-6"> <div class="mb-6">
<x-input-label for="title" :value="__('Title')" /> <x-input-label for="title" :value="__('Title')" />
<x-text-input id="title" name="title" type="text" class="mt-1 block w-full" <x-text-input id="title" name="title" type="text" class="mt-1 block w-full"
:value="old('title', $event->meta->title)" required autofocus /> :value="old('title', $event->meta?->title ?? '')" required autofocus />
<x-input-error class="mt-2" :messages="$errors->get('title')" /> <x-input-error class="mt-2" :messages="$errors->get('title')" />
</div> </div>
@ -38,7 +38,7 @@
<div class="mb-6"> <div class="mb-6">
<x-input-label for="description" :value="__('Description')" /> <x-input-label for="description" :value="__('Description')" />
<textarea id="description" name="description" rows="3" <textarea id="description" name="description" rows="3"
class="mt-1 block w-full rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring">{{ old('description', $event->meta->description) }}</textarea> class="mt-1 block w-full rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring">{{ old('description', $event->meta?->description ?? '') }}</textarea>
<x-input-error class="mt-2" :messages="$errors->get('description')" /> <x-input-error class="mt-2" :messages="$errors->get('description')" />
</div> </div>
@ -46,7 +46,7 @@
<div class="mb-6"> <div class="mb-6">
<x-input-label for="location" :value="__('Location')" /> <x-input-label for="location" :value="__('Location')" />
<x-text-input id="location" name="location" type="text" class="mt-1 block w-full" <x-text-input id="location" name="location" type="text" class="mt-1 block w-full"
:value="old('location', $event->meta->location)" /> :value="old('location', $event->meta?->location ?? '')" />
<x-input-error class="mt-2" :messages="$errors->get('location')" /> <x-input-error class="mt-2" :messages="$errors->get('location')" />
</div> </div>
@ -57,9 +57,8 @@
<x-input-label for="start_at" :value="__('Starts')" /> <x-input-label for="start_at" :value="__('Starts')" />
<x-text-input id="start_at" name="start_at" type="datetime-local" <x-text-input id="start_at" name="start_at" type="datetime-local"
class="mt-1 block w-full" class="mt-1 block w-full"
:value="old('start_at', $event->meta->start_at :value="old('start_at', $start)"
? $event->meta->start_at->format('Y-m-d\TH:i') required />
: '')" required />
<x-input-error class="mt-2" :messages="$errors->get('start_at')" /> <x-input-error class="mt-2" :messages="$errors->get('start_at')" />
</div> </div>
@ -67,9 +66,8 @@
<x-input-label for="end_at" :value="__('Ends')" /> <x-input-label for="end_at" :value="__('Ends')" />
<x-text-input id="end_at" name="end_at" type="datetime-local" <x-text-input id="end_at" name="end_at" type="datetime-local"
class="mt-1 block w-full" class="mt-1 block w-full"
:value="old('end_at', $event->meta->end_at :value="old('end_at', $end)"
? $event->meta->end_at->format('Y-m-d\TH:i') required />
: '')" required />
<x-input-error class="mt-2" :messages="$errors->get('end_at')" /> <x-input-error class="mt-2" :messages="$errors->get('end_at')" />
</div> </div>
@ -78,7 +76,7 @@
{{-- All-day --}} {{-- All-day --}}
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
<input id="all_day" name="all_day" type="checkbox" value="1" <input id="all_day" name="all_day" type="checkbox" value="1"
@checked(old('all_day', $event->meta->all_day)) /> @checked(old('all_day', $event->meta?->all_day)) />
<label for="all_day" class="ms-2 text-sm text-gray-700"> <label for="all_day" class="ms-2 text-sm text-gray-700">
{{ __('All day event') }} {{ __('All day event') }}
</label> </label>

View File

@ -5,6 +5,7 @@ use App\Http\Controllers\ProfileController;
use App\Http\Controllers\CalendarController; use App\Http\Controllers\CalendarController;
use App\Http\Controllers\EventController; use App\Http\Controllers\EventController;
use App\Http\Controllers\DavController; use App\Http\Controllers\DavController;
use App\Http\Controllers\SubscriptionController;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
/* /*
@ -56,9 +57,13 @@ require __DIR__.'/auth.php';
| authentication for DAV clients. | authentication for DAV clients.
*/ */
// default dav handling
Route::match( Route::match(
['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'REPORT', 'LOCK', 'UNLOCK'], ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'REPORT', 'LOCK', 'UNLOCK'],
'/dav/{any?}', '/dav/{any?}',
[DavController::class, 'handle'] [DavController::class, 'handle']
)->where('any', '.*') )->where('any', '.*')
->withoutMiddleware([VerifyCsrfToken::class]); ->withoutMiddleware([VerifyCsrfToken::class]);
// subscriptions
Route::get('/subscriptions/{calendarUri}.ics', [SubscriptionController::class, 'download']);