210 lines
5.7 KiB
Go
210 lines
5.7 KiB
Go
package user
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/text/cases"
|
|
"golang.org/x/text/language"
|
|
|
|
"crussell/db"
|
|
"crussell/handlers/auth"
|
|
"crussell/mw"
|
|
)
|
|
|
|
var titleCaser = cases.Title(language.English)
|
|
|
|
type UserProfile struct {
|
|
ID string `json:"id"`
|
|
Email string `json:"email"`
|
|
FirstName string `json:"firstName"`
|
|
LastName string `json:"lastName"`
|
|
Phone *string `json:"phone,omitempty"`
|
|
DateOfBirth *string `json:"dateOfBirth,omitempty"`
|
|
Role string `json:"role"`
|
|
LoyaltyStamps int `json:"loyaltyStamps"`
|
|
ReferralCode string `json:"referralCode"`
|
|
ProfilePicURL *string `json:"profilePicUrl,omitempty"`
|
|
}
|
|
|
|
type UpdateProfileRequest struct {
|
|
FirstName string `json:"firstName"`
|
|
LastName string `json:"lastName"`
|
|
Phone string `json:"phone"`
|
|
}
|
|
|
|
// GET /api/user/profile
|
|
func GetProfileHandler(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := mw.GetUserID(r.Context())
|
|
if !ok {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
var user UserProfile
|
|
err := db.DB.QueryRow(r.Context(), `
|
|
SELECT
|
|
id, email, n_first_name, n_last_name, phone,
|
|
date_of_birth::text, account_role, loyalty_stamps,
|
|
referral_code, profile_pic_url
|
|
FROM users
|
|
WHERE id = $1
|
|
`, userID).Scan(
|
|
&user.ID, &user.Email, &user.FirstName, &user.LastName,
|
|
&user.Phone, &user.DateOfBirth, &user.Role,
|
|
&user.LoyaltyStamps, &user.ReferralCode, &user.ProfilePicURL,
|
|
)
|
|
|
|
if err != nil {
|
|
http.Error(w, "user not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(user)
|
|
}
|
|
|
|
// PUT /api/user/profile
|
|
// updateCardDAV updates an existing contact in SabreDAV using user ID
|
|
func updateCardDAV(userID, firstName, lastName, email, phone, dob string) error {
|
|
// Use user ID as filename - consistent with registration
|
|
filename := fmt.Sprintf("%s.vcf", userID)
|
|
url := fmt.Sprintf("http://nginx/dav/addressbooks/principals/default/default/%s", filename)
|
|
|
|
// Create vCard with user ID as UID (no need to fetch existing)
|
|
timestamp := time.Now().UTC().Format("20060102T150405Z")
|
|
uid := fmt.Sprintf("%s@example.com", userID)
|
|
|
|
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
|
|
REV:%s
|
|
END:VCARD`, uid, firstName, lastName, lastName, firstName, email, phone, dob, timestamp)
|
|
|
|
// PUT updated vCard
|
|
req, err := http.NewRequest("PUT", url, bytes.NewBufferString(vcard))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "text/vcard; charset=utf-8")
|
|
req.SetBasicAuth("admin", "admin")
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
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
|
|
}
|
|
|
|
// PUT /api/user/profile
|
|
func UpdateProfileHandler(w http.ResponseWriter, r *http.Request) {
|
|
userID, _ := mw.GetUserID(r.Context())
|
|
|
|
var req UpdateProfileRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Normalize input
|
|
req.FirstName = strings.TrimSpace(req.FirstName)
|
|
req.LastName = strings.TrimSpace(req.LastName)
|
|
req.Phone = strings.TrimSpace(req.Phone)
|
|
|
|
// Required fields
|
|
if req.FirstName == "" || req.LastName == "" || req.Phone == "" {
|
|
http.Error(w, "first name, last name and phone are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate names (unicode letters, spaces, hyphen, apostrophe, dot)
|
|
nameRegex := regexp.MustCompile(`^[\p{L}\p{M}\s\-'\.]+$`)
|
|
|
|
if !nameRegex.MatchString(req.FirstName) || !nameRegex.MatchString(req.LastName) {
|
|
http.Error(w, "invalid characters in name", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate lengths
|
|
if len(req.FirstName) < 1 || len(req.FirstName) > 50 {
|
|
http.Error(w, "first name must be 1-50 characters", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if len(req.LastName) < 1 || len(req.LastName) > 50 {
|
|
http.Error(w, "last name must be 1-50 characters", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Normalize phone (strip spaces, hyphens, brackets)
|
|
req.Phone = strings.Map(func(r rune) rune {
|
|
if (r >= '0' && r <= '9') || r == '+' {
|
|
return r
|
|
}
|
|
return -1
|
|
}, req.Phone)
|
|
|
|
// Validate UK phone number
|
|
phone, err := auth.ValidateUKPhoneNumber(req.Phone)
|
|
if err != nil {
|
|
http.Error(w, "invalid phone number format", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.Phone = strings.TrimSpace(phone)
|
|
|
|
// Title case names
|
|
req.FirstName = titleCaser.String(strings.ToLower(req.FirstName))
|
|
req.LastName = titleCaser.String(strings.ToLower(req.LastName))
|
|
|
|
// Fetch user's email and DOB for CardDAV update
|
|
var email string
|
|
var dob time.Time
|
|
err = db.DB.QueryRow(r.Context(), `
|
|
SELECT email, date_of_birth FROM users WHERE id = $1
|
|
`, userID).Scan(&email, &dob)
|
|
|
|
if err != nil {
|
|
http.Error(w, "failed to fetch user data", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Update DB
|
|
_, err = db.DB.Exec(r.Context(), `
|
|
UPDATE users
|
|
SET n_first_name = $1, n_last_name = $2, phone = $3, updated_at = NOW()
|
|
WHERE id = $4
|
|
`, req.FirstName, req.LastName, req.Phone, userID)
|
|
|
|
if err != nil {
|
|
http.Error(w, "update failed", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Update CardDAV (non-blocking)
|
|
go func() {
|
|
dobStr := dob.Format("2006-01-02")
|
|
if err := updateCardDAV(userID, req.FirstName, req.LastName, email, req.Phone, dobStr); err != nil {
|
|
fmt.Printf("Warning: Failed to update CardDAV contact for user %s: %v\n", userID, err)
|
|
}
|
|
}()
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|