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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user