From 8d9f72b9f096af15207869f91bec6b5539fd00c8 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Sun, 16 Aug 2026 00:20:50 +0100 Subject: [PATCH] feat: CardDAV profile sync writes through dav.Service (photo included, DAV_BASE_URL retired) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - profile.go updateCardDAV now writes directly to the shared dav_cards table via dav.Service (mirroring registration) instead of PUTting a vCard to DAV_BASE_URL over HTTP — the Go backend no longer needs the SabreDAV URL, only the sabredav PHP container uses the server-side credential. - dav/types.go: ContactInput gains PhotoURL; GenerateVCard emits the PHOTO;VALUE=URI line when set, so the profile photo syncs into the address book. DAV_BASE_URL is retained in .env.example for reference only. --- backend/handlers/user/profile.go | 70 +++++++++----------------------- backend/internal/dav/types.go | 8 +++- 2 files changed, 26 insertions(+), 52 deletions(-) diff --git a/backend/handlers/user/profile.go b/backend/handlers/user/profile.go index 6e02c4f..0026e01 100644 --- a/backend/handlers/user/profile.go +++ b/backend/handlers/user/profile.go @@ -27,6 +27,7 @@ import ( "crussell/db" "crussell/handlers/auth" "crussell/handlers/payments" + "crussell/internal/dav" "crussell/internal/images" "crussell/internal/s3" "crussell/internal/validators" @@ -207,62 +208,29 @@ func GetProfileHandler(w http.ResponseWriter, r *http.Request) { } // PUT /api/user/profile -// updateCardDAV updates an existing contact in SabreDAV using user ID. -// The DAV base URL is configured via DAV_BASE_URL — if unset, the update is skipped -// silently (allowing the handler to work in dev environments without a DAV server). +// updateCardDAV syncs the updated profile into the CardDAV address book. +// It writes directly to the shared dav_cards table via dav.Service (the same +// table the SabreDAV server serves), mirroring how registration creates +// contacts. Returns nil if no DAV backend is configured, so the handler works +// in dev environments without a DAV server. func updateCardDAV(userID, firstName, lastName, email, phone, dob, profilePicURL string) error { - davBase := os.Getenv("DAV_BASE_URL") - if davBase == "" { + if dav.Service == nil { return nil } - filename := fmt.Sprintf("%s.vcf", userID) - url := fmt.Sprintf("%s/addressbooks/principals/default/default/%s", davBase, filename) - - timestamp := clock.Now().UTC().Format("20060102T150405Z") - uid := fmt.Sprintf("%s@example.com", userID) - - var photoLine string - if profilePicURL != "" { - photoLine = fmt.Sprintf("PHOTO;VALUE=URI:%s", profilePicURL) + input := dav.ContactInput{ + UserID: userID, + FirstName: firstName, + LastName: lastName, + Email: email, + Phone: phone, + DOB: dob, + PhotoURL: profilePicURL, } - - vcard := fmt.Sprintf(`BEGIN:VCARD -VERSION:3.0 -UID:%s -FN:%s %s -N:%s;%s;;; -EMAIL;TYPE=INTERNET:%s -TEL;TYPE=CELL:%s -BDAY:%s -%s -REV:%s -END:VCARD`, uid, firstName, lastName, lastName, firstName, email, phone, dob, photoLine, timestamp) - - // PUT updated vCard - req, err := http.NewRequest("PUT", url, bytes.NewBufferString(vcard)) // #nosec G704 — internal CardDAV server - if err != nil { - return fmt.Errorf("failed to create request: %w", err) + uri := fmt.Sprintf("%s.vcf", userID) + if _, err := dav.Service.GetContactByURI(1, uri); err != nil { + return dav.Service.CreateContact(1, userID, input) } - - req.Header.Set("Content-Type", "text/vcard; charset=utf-8") - davPassword := os.Getenv("DAV_ADMIN_PASSWORD") - if davPassword == "" { - davPassword = "admin" - } - req.SetBasicAuth("admin", davPassword) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) // #nosec G704 — internal CardDAV server - if err != nil { - return fmt.Errorf("failed to update CardDAV: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("CardDAV returned status: %d", resp.StatusCode) - } - - return nil + return dav.Service.UpdateContact(1, uri, input) } // PUT /api/user/profile diff --git a/backend/internal/dav/types.go b/backend/internal/dav/types.go index dd55f29..2abcd9f 100644 --- a/backend/internal/dav/types.go +++ b/backend/internal/dav/types.go @@ -77,6 +77,7 @@ type ContactInput struct { Email string Phone string DOB string + PhotoURL string } // ============================================================================ @@ -135,6 +136,10 @@ END:VCALENDAR`, uid, dtstamp, dtstart, dtend, // GenerateVCard creates vCard format (version 3.0) func GenerateVCard(input ContactInput) string { + var photoLine string + if input.PhotoURL != "" { + photoLine = fmt.Sprintf("PHOTO;VALUE=URI:%s\n", input.PhotoURL) + } vcard := fmt.Sprintf(`BEGIN:VCARD VERSION:3.0 UID:%s @@ -143,7 +148,7 @@ N:%s;%s;;; EMAIL;TYPE=INTERNET:%s TEL;TYPE=CELL:%s BDAY:%s -REV:%s +%sREV:%s END:VCARD`, input.UserID, input.FirstName, input.LastName, @@ -151,6 +156,7 @@ END:VCARD`, input.Email, input.Phone, input.DOB, + photoLine, clock.Now().UTC().Format("20060102T150405Z")) return vcard