user profiles
All checks were successful
pedestrian-simulator / build (push) Successful in 1m17s

This commit is contained in:
2026-01-13 13:10:55 -07:00
parent f985d3433d
commit 72b94597ca
6 changed files with 478 additions and 45 deletions

View File

@@ -287,16 +287,41 @@ func HandleKMLList(w http.ResponseWriter, r *http.Request) {
if publicSortBy == "" {
publicSortBy = "votes"
}
targetUserID := r.URL.Query().Get("target_user_id")
// 1. Get My Files
myFiles, myTotal, err := fetchKMLList(userID, true, myPage, myLimit, mySortBy)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
// 1. Get My Files (if no specific target user is requested, or if target is me)
// logic: If targetUserID is present and != userID, we don't show "My Files" (which implies private/editable).
// But the UI might expect the "My Files" block to just be empty.
// Actually, the prompt implies "My Files" is the logged-in user's files.
// If we are viewing a profile, we just want that user's PUBLIC files.
// The current frontend calls this endpoint to populate the standard browser "My Files" and "Public Files" tabs.
// We should probably keep this behavior for the standard view.
// If it's a profile request, we might just be using the public list with a filter.
// Let's rely on the client to ask for what it wants.
// Existing client: Calls /list without target_user_id. Expects: My Files (all mine), Public Files (all public).
// New Client (Profile): Needs distinct endpoint or param?
// If we add `target_user_id` to the public files query, we can reuse this.
// 1. Get My Files (Logged in user's files)
var myFiles []KMLMetadata
var myTotal int
var err error
if targetUserID == "" {
myFiles, myTotal, err = fetchKMLList(userID, true, myPage, myLimit, mySortBy)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// 2. Get Public Files
publicFiles, publicTotal, err := fetchKMLList(userID, false, publicPage, publicLimit, publicSortBy)
var publicFiles []KMLMetadata
var publicTotal int
publicFiles, publicTotal, err = fetchKMLListPublic(userID, targetUserID, publicPage, publicLimit, publicSortBy)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -315,15 +340,29 @@ func HandleKMLList(w http.ResponseWriter, r *http.Request) {
}
func fetchKMLList(userID string, mineOnly bool, page, limit int, sortBy string) ([]KMLMetadata, int, error) {
// Standard fetch for current user or general public list (wrapper)
return fetchKMLQuery(userID, "", mineOnly, page, limit, sortBy)
}
func fetchKMLListPublic(requestingUserID, targetUserID string, page, limit int, sortBy string) ([]KMLMetadata, int, error) {
return fetchKMLQuery(requestingUserID, targetUserID, false, page, limit, sortBy)
}
func fetchKMLQuery(userID, targetUserID string, mineOnly bool, page, limit int, sortBy string) ([]KMLMetadata, int, error) {
offset := (page - 1) * limit
var whereClause string
var args []interface{}
if mineOnly {
whereClause = "WHERE m.user_id = ?"
args = append(args, userID)
} else {
whereClause = "WHERE m.is_public = 1"
if targetUserID != "" {
whereClause += " AND m.user_id = ?"
args = append(args, targetUserID)
}
}
// Get total count first
@@ -609,3 +648,31 @@ func HandleKMLDownload(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
w.Write(data)
}
// HandleUserProfile serves public user profile data
func HandleUserProfile(w http.ResponseWriter, r *http.Request) {
// Authentication optional? Yes, profiles should be public.
// But let's check auth just to be safe if we want to restrict it to logged-in users later.
// Current requirement: "implement profile pages... open the KML details overlay... so that you can do it too".
// Implies logged in users mostly, but let's allow it generally if the files are public.
// Get target user ID from URL
targetID := r.URL.Query().Get("user_id")
if targetID == "" {
http.Error(w, "Missing user_id", http.StatusBadRequest)
return
}
user, err := GetPublicUser(targetID)
if err != nil {
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
if user == nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}

View File

@@ -137,7 +137,10 @@ func main() {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
})
// 6. Start Server
// 7. User Profile Endpoint
http.HandleFunc("/api/user/profile", RequireAuth(HandleUserProfile))
// 8. Start Server
binding := "0.0.0.0:8080"
fmt.Printf("Server starting on http://%s\n", binding)
log.Fatal(http.ListenAndServe(binding, RecoveryMiddleware(http.DefaultServeMux)))

View File

@@ -30,6 +30,21 @@ func GetUser(fitbitUserID string) (*User, error) {
return &user, nil
}
// GetPublicUser retrieves public user info by Fitbit user ID
func GetPublicUser(fitbitUserID string) (*User, error) {
var user User
err := db.QueryRow("SELECT fitbit_user_id, display_name, avatar_url, created_at FROM users WHERE fitbit_user_id = ?", fitbitUserID).
Scan(&user.FitbitUserID, &user.DisplayName, &user.AvatarURL, &user.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
// Ensure we don't return sensitive info if struct expands later, though current struct is safe for public
return &user, nil
}
// CreateOrUpdateUser adds or updates a user in the database
func CreateOrUpdateUser(fitbitUserID, displayName, avatarURL string) (*User, error) {
_, err := db.Exec(`