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:
parent
8d0738019f
commit
a82d9fe01f
20
.env.example
20
.env.example
@ -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
|
||||||
|
@ -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 shouldn’t
|
$instance = $calendar->instances->first(); // may be null but shouldn’t
|
||||||
@ -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'] ?? '',
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 Sabre’s calendarobjects + meta row
|
* insert vevent into sabre’s 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
|
||||||
|
67
app/Http/Controllers/SubscriptionController.php
Normal file
67
app/Http/Controllers/SubscriptionController.php
Normal 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 ?? '');
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 . '/'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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']);
|
||||||
|
Loading…
Reference in New Issue
Block a user