Initial commit. Working login, example UI with prototype and demo, connections to DB and DAV, local and prod setups.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// DELETE /api/user/account
|
||||
func DeleteAccountHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Delete user's data
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"crussell/db"
|
||||
"crussell/mw"
|
||||
)
|
||||
|
||||
type LoyaltyResponse struct {
|
||||
Stamps int `json:"stamps"`
|
||||
ReferralCode string `json:"referralCode"`
|
||||
}
|
||||
|
||||
// GET /api/user/loyalty
|
||||
func GetLoyaltyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := mw.GetUserID(r.Context())
|
||||
|
||||
var loyalty LoyaltyResponse
|
||||
err := db.DB.QueryRow(r.Context(), `
|
||||
SELECT loyalty_stamps, referral_code
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`, userID).Scan(&loyalty.Stamps, &loyalty.ReferralCode)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "failed to get loyalty info", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(loyalty)
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user