Commit Graph
16 Commits
Author SHA1 Message Date
arnef 8e946964ec fix(caldav): expand RRULE for recurring events 2026-09-06 15:49:09 +02:00
arnef f34041d889 feat(web): show ICS subscription events in detail view 2026-09-02 20:02:48 +02:00
arnef e8c4dadfea chore: fmt code 2026-08-30 19:58:06 +02:00
arnef d3780e9e32 refactor: align import paths with module name 2026-08-30 18:51:45 +02:00
arnefandCopilot d0e1dab837 Use display name instead of username for shared calendar owners
Add db.DisplayName(username), returning the user's display name if
set, otherwise the raw username. Use it everywhere a shared calendar's
owner is shown: the web calendar legend ("shared by ..."), the "new
event"/edit calendar select label, the dashboard's "Shared with you"
list, and the CalDAV-side description/name-collision disambiguation
("Name (Owner)") shown to CalDAV clients like DAVx5.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-21 06:41:51 +02:00
arnefandCopilot b58a6ba4ea Disambiguate shared calendars with a name colliding with the owner's
If a calendar shared with a user has the same name as one of the user's
own calendars (or as another share from a different owner), DAVx5 would
show two calendars with an identical, indistinguishable title. The
shared calendar's CalDAV display name (Calendar.Name, exposed via
displayname/DAVx5's calendar list) now gets the owning user's username
appended in parentheses in that case, e.g. "personal (alice)" — the
underlying calendar path/URL is unaffected, and a user's own calendars
are never renamed (only shared ones can collide with something else).

internal/caldav/backend.go: calendarMeta split into calendarMeta (own
calendars, unchanged name) and calendarMetaNamed (explicit display name);
new sharedDisplayName(requester, owner, realName) checks requester's own
calendars and other shares for a collision. Wired into both
ListCalendars and GetCalendar (the latter used by direct
PROPFIND/REPORT against a shared calendar's own URL).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 23:20:17 +02:00
arnefandCopilot 213b22f821 Fix 500 on REPORT for recurring events with Windows timezone IDs
DAVx5 reported a 500 Internal Server Error on a time-range REPORT
against a calendar containing a recurring event whose DTSTART/DTEND used
a Windows-style TZID (e.g. "W. Europe Standard Time", as commonly written
by Outlook/Exchange and some Thunderbird/Lightning setups) instead of an
IANA zone name.

go-ical resolves TZID via a plain time.LoadLocation call, which only
understands IANA names. A simple decode of such an event succeeds (RRULE
dates aren't parsed eagerly), but expanding its recurrence - which
go-webdav's caldav.Filter does for every time-range REPORT - calls
Component.RecurrenceSet, which does call time.LoadLocation(tzid) and
fails with "ical: error parsing start time: unknown time zone ...".

Add internal/icalfix, a small shared helper that rewrites recognized
Windows timezone identifiers (TZID parameters and VTIMEZONE TZID: lines)
to their IANA equivalent in raw ICS bytes before decoding. Wire it into
every ical.NewDecoder call site: internal/caldav/backend.go's
decodeObject (fixes the reported bug), internal/web/calendar.go's event
rendering/ICS import, and internal/icssub's remote feed fetching, so a
subscribed feed with the same issue doesn't hit it either.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 22:23:11 +02:00
arnefandCopilot 1707ab4060 Add ICS/webcal HTTP subscription calendars
Users can now subscribe to a remote ICS/webcal feed from the dashboard
(name + color), the same way they set up a real calendar or the virtual
Birthdays calendar. Subscriptions are read-only, per-user, and share the
"/cal/home/<name>/" namespace with real calendars and "birthdays" (name
collisions are rejected in both directions).

- internal/db: new ics_subscriptions table + CRUD (internal/db/ics.go);
  CreateCalendarWithColor checks for a colliding subscription name.
- internal/icssub: shared HTTP-fetch + TTL cache (15 min) for remote ICS
  calendars, with webcal:// -> https:// rewriting and stale-on-error
  fallback, used by both the web UI and the CalDAV backend.
- internal/web: dashboard "Subscribe to an ICS/webcal calendar" form,
  color picker, delete button (internal/web/ics.go,
  templates/dashboard.templ); month view renders subscription events in
  their chosen color, read-only (internal/web/calendar.go).
- internal/caldav: subscriptions are exposed as read-only calendars
  (internal/caldav/ics.go) - listed in PROPFIND, events served via
  GET/REPORT, PUT/DELETE on individual events rejected with 403, but
  DELETE on the calendar itself unsubscribes; calendar-color is injected
  the same way as for real calendars and Birthdays.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 22:10:49 +02:00
arnefandCopilot b451f1a76e Make birthdays calendar color configurable and expose it via CalDAV
- Add a per-user birthday_calendars table (color, cascade-deletes with
  the user) and GetBirthdayCalendarColor/SetBirthdayCalendarColor in
  internal/db, plus a reservedCalendarNames guard ("birthdays") in
  CreateCalendarWithColor so no real calendar can collide with the
  synthetic one, whether created via the web UI, nidusctl, or CalDAV
  MKCALENDAR.
- Add a "Birthdays" virtual resource card to the dashboard (color
  picker only, no delete/share controls) backed by a new
  ResourceCard.Virtual flag and POST /web/resources/birthdays/color
  handler.
- Extract the birthday-parsing/generation logic shared by the web
  calendar view and CalDAV into internal/birthdays (ParseBirthday,
  Collect, OccurrenceDate, Summary) instead of duplicating it.
- Expose the Birthdays calendar over real CalDAV in
  internal/caldav/backend.go + birthdays.go: it's always listed for
  every user, generates one VEVENT per (contact, year) for a rolling
  window (current year -2..+8) with "🎂 Name (Age)" titles, is
  read-only (Put/Delete/DeleteCalendar all return 403), and its
  Apple/DAVx5 calendar-color is injected from the same per-user
  setting used by the dashboard/web view.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 20:09:59 +02:00
arnefandCopilot 65cd24a192 Show plain calendar name for shared calendars, drop ~owner prefix
DAVx5/CalDAV clients displayed a shared calendar's DAV:displayname as
"alice~work" (the synthetic owner~name local identifier nidus uses
internally to keep a shared calendar's URL unique per subscriber).
That internal name is still needed for routing (it's still what appears
in the calendar's URL path), but there's no reason to show it to the
end user as the calendar's label.

calendarMeta() now always sets caldav.Calendar.Name to the plain
calendar name (e.g. "work"), while Path still uses the "owner~name"
local name. The web UI already showed the plain name via a separate
"shared by <owner>" entry, so it's unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 10:09:41 +02:00
arnefandCopilot 5f72e58ad3 Fix calendar-color not applied by DAVx5 (duplicate propstat)
DAVx5 explicitly requests the Apple calendar-color property by name.
go-webdav's stock property map doesn't know it, so it always emitted a
404 Not Found propstat for it. Our injection was adding a *second*,
200 OK propstat with the color into the same <response>, producing a
response with two propstats for the same property name — invalid
multistatus that real clients (dav4jvm/DAVx5) resolved by preferring the
404, so the color was silently ignored.

Now the bogus 404-only propstat for calendar-color is stripped before
injecting the 200 OK one, leaving a single, valid propstat per response.
Verified against Nextcloud's documented behavior (single propstat with
the color) and against a live PROPFIND matching DAVx5's actual request
shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 07:04:49 +02:00
arnefandCopilot ebbc7a2a2b Add calendar color support (DAVx5 calendar-color)
Calendars can now have a color (hex, e.g. #3b82f6) that DAVx5 and other
CalDAV clients pick up via the Apple/dav4jvm calendar-color property.

- db: add calendars.color column with migration for existing DBs;
  CreateCalendarWithColor, SetCalendarColor, GetCalendarColor;
  ListCalendars now returns []Calendar{Name, Color} instead of []string
- caldav: since go-webdav's caldav.Backend interface has no extension
  point for vendor properties, wrap the handler with a response-rewriting
  middleware that injects <calendar-color xmlns="http://apple.com/ns/ical/">
  into PROPFIND responses for calendars that have a color set
- web: color picker on the "New calendar" form and an inline color swatch/
  picker on each calendar card (calendars only, not address books)
- nidusctl: `calendar create --color` flag and a new `calendar color`
  subcommand; `calendar list` now also prints the color if set

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 06:43:56 +02:00
arnefandCopilot 7d4f28de3c Move users, calendars, and address books from config.yaml into the database
BREAKING CHANGE: the users:/config-based collection setup is gone. All
user, calendar, and address-book data now lives in the SQLite DB
(internal/db) and is managed exclusively via nidusctl or the web UI.
Existing deployments must recreate their users after upgrading:
  nidusctl user create <username>
  nidusctl calendar create <username> <name>
  nidusctl addressbook create <username> <name>

- internal/db: new users, calendars, addressbooks tables with FK cascade
  delete; foreign_keys pragma enabled; internal/db/users.go implements
  full CRUD + bcrypt auth (CreateUser, VerifyPassword, ListUsers,
  CreateCalendar/AddressBook, etc).
- internal/config: removed Users/UserConfig entirely.
- internal/auth: Basic Auth now checks credentials via db.DB instead of
  cfg.Users.
- internal/caldav, internal/carddav: ListCalendars/ListAddressBooks and
  Create/Delete now backed by the DB.
- internal/web: login uses db.VerifyPassword; new resources.go adds
  create/delete handlers for calendars/address books at
  /web/resources/{calendar,addressbook}; dashboard gained create forms
  and per-card delete buttons (templ + htmx, no hyperscript).
- tools/nidusctl: new user create/delete/list/passwd commands (masked
  interactive password prompt via golang.org/x/term) plus create/delete/
  list subcommands for calendar/addressbook.
- cmd/server/main.go: pre-creates on-disk collections from the DB at
  startup instead of cfg.Users; warns when no users exist yet.
- Updated tests to seed data via the DB; added resources_test.go for the
  new web UI handlers.
- README.md and .github/copilot-instructions.md updated to document the
  new nidusctl commands and the DB-backed architecture.

Verified end-to-end against a live test server: nidusctl user/calendar/
addressbook create, DAV Basic Auth PROPFIND, web login, dashboard
rendering, and web UI create/delete of resources all confirmed working.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-19 12:43:34 +02:00
arnefandCopilot daa51d62b1 Add calendar/address-book sharing backend
Introduce internal/db, a small SQLite-backed store (pure-Go
modernc.org/sqlite, no CGO) at <data_dir>/nidus.db holding
calendar_shares and addressbook_shares grant tables (owner, resource
name, shared-with user, read/write permission). This is the first step
towards user management and a web UI: a real datastore that a future
admin CLI/UI can build on, instead of the static config.yaml.

Wire sharing into the CalDAV/CardDAV backends:
- ListCalendars/ListAddressBooks now also include resources shared with
  the requesting user, exposed under the synthetic local name
  "<owner>~<name>" in the grantee's own home-set — no separate account,
  no data copying, the object still physically lives under the owner's
  store.Store namespace.
- All read paths (Get/List/QueryCalendarObjects, address book
  equivalents) resolve the synthetic name back to (owner, real name) and
  require any share (read or write) to exist.
- All write paths (Put/Delete object, DeleteCalendar/AddressBook)
  additionally require a write-permission share; read-only shares get a
  403 Forbidden.
- CreateCalendar/CreateAddressBook remain scoped to the acting user's own
  namespace — sharing an existing collection is done via ShareCalendar/
  ShareAddressBook, not by creating one directly in someone else's name.

Add internal/db/shares_test.go (grant/lookup/update/unshare/list
semantics) and internal/{caldav,carddav}/backend_test.go (shared
calendar/address book visibility, write permission enforcement,
unauthorized access rejection). Update README (features, new "Sharing
calendars and address books" section, project layout, dependencies) and
copilot-instructions.md to document the new package and sharing model.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-18 20:10:31 +02:00
arnefandCopilot b4644bc590 Fix WebDAV/CalDAV/CardDAV bugs, drop username from URLs, harden concurrency
- Root handler now only serves the welcome page for GET/HEAD; all other
  methods (e.g. OPTIONS, PROPFIND) return 405 with an Allow header instead
  of always returning 200, fixing client capability probes and PROPFIND
  misbehavior.
- Mount /files/ properly and cache one xwebdav.Handler per authenticated
  user so its LockSystem persists across requests instead of being
  recreated per-request (which broke LOCK/UNLOCK).
- Remove the username segment from all DAV URLs (/cal/, /card/, /files/
  are now identical for every account; the acting user is always resolved
  via Basic Auth, never the path).
- Reintroduce a fixed literal "home" path segment (/cal/home/,
  /card/home/) to preserve the URL segment depth that go-webdav's
  caldav/carddav server relies on to classify resources (principal vs.
  home-set vs. collection vs. object). Removing the username had
  collapsed this depth, silently misclassifying requests and returning
  empty <multistatus> responses (DAVx5 "no resources found").
- Replace the store's single global mutex with per-user sharded locks so
  different users' requests no longer serialize against each other.
- Add auth.NewContext test helper, WebDAV handler tests
  (per-user isolation, lock persistence across requests), and a
  concurrent multi-user store test.
- Update README and copilot-instructions to document the new URL scheme
  and the go-webdav path-depth classification quirk.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-18 12:49:57 +02:00
arnef 7a11b5bbbf wip 2026-04-23 21:56:59 +02:00