feat: CardDAV profile sync writes through dav.Service (photo included, DAV_BASE_URL retired)

- 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.
This commit is contained in:
2026-08-22 00:34:50 +01:00
parent 8dcfe52ac8
commit 8d9f72b9f0
2 changed files with 26 additions and 52 deletions
+19 -51
View File
@@ -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
+7 -1
View File
@@ -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