package api import ( "encoding/json" "fmt" "net/http" "net/url" "strings" ) // shareRecord is the PocketBase-facing shape of a car_shares row: a grant that // lets `user` access `car` at `permission` ("read"/"write"). type shareRecord struct { ID string `json:"id"` Car string `json:"car"` User string `json:"user"` Permission string `json:"permission"` Created string `json:"created"` } // shareView is the API shape returned to clients: the grantee's public identity // plus their permission on the car. type shareView struct { User userInfo `json:"user"` Permission string `json:"permission"` } // requireCarOwner ensures the current user owns the car in the {id} path param. // Sharing management is owner-only. Returns the car id on success, else writes // the response and returns "". func (s *Server) requireCarOwner(w http.ResponseWriter, r *http.Request) string { carID := r.PathValue("id") level, _, err := s.carAccessLevel(r.Context(), s.currentUserID(r), carID) if err != nil { writePBError(w, err) return "" } if level != accessOwner { writeError(w, http.StatusForbidden, "only the owner can manage sharing") return "" } return carID } // handleListShares serves GET /api/cars/{id}/shares (owner only). func (s *Server) handleListShares(w http.ResponseWriter, r *http.Request) { carID := s.requireCarOwner(w, r) if carID == "" { return } res, err := s.pb.List(r.Context(), colShares, url.Values{ "filter": {fmt.Sprintf("car='%s'", carID)}, "perPage": {"200"}, }) if err != nil { writePBError(w, err) return } var recs []shareRecord if err := json.Unmarshal(res.Items, &recs); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } out := make([]shareView, 0, len(recs)) for _, rec := range recs { u, err := s.fetchUser(r, rec.User) if err != nil { continue // grantee user was deleted; skip defensively } out = append(out, shareView{ User: userInfo{ID: u.ID, Email: u.Email, Name: u.Name}, Permission: rec.Permission, }) } writeJSON(w, http.StatusOK, out) } type shareRequest struct { Email string `json:"email"` Permission string `json:"permission"` } // handleUpsertShare serves POST /api/cars/{id}/shares (owner only). It grants // or updates a share for the user identified by email. Body: {email, // permission}. Idempotent: an existing grant for that user is updated. func (s *Server) handleUpsertShare(w http.ResponseWriter, r *http.Request) { carID := s.requireCarOwner(w, r) if carID == "" { return } var in shareRequest if err := decodeJSON(r, &in); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } in.Email = strings.TrimSpace(strings.ToLower(in.Email)) if in.Email == "" { writeError(w, http.StatusBadRequest, "email is required") return } if in.Permission != accessRead && in.Permission != accessWrite { writeError(w, http.StatusBadRequest, "permission must be 'read' or 'write'") return } target, err := s.findUserByEmail(r, in.Email) if err != nil { writePBError(w, err) return } if target == nil { writeError(w, http.StatusNotFound, "no user with that email") return } if target.ID == s.currentUserID(r) { writeError(w, http.StatusBadRequest, "you already own this car") return } // Upsert: update the existing grant's permission, else create a new one. existing, err := s.findShare(r, carID, target.ID) if err != nil { writePBError(w, err) return } payload := map[string]any{"car": carID, "user": target.ID, "permission": in.Permission} if existing != nil { if err := s.pb.Update(r.Context(), colShares, existing.ID, payload, nil); err != nil { writePBError(w, err) return } } else if err := s.pb.Create(r.Context(), colShares, payload, nil); err != nil { writePBError(w, err) return } writeJSON(w, http.StatusOK, shareView{ User: userInfo{ID: target.ID, Email: target.Email, Name: target.Name}, Permission: in.Permission, }) } // handleDeleteShare serves DELETE /api/cars/{id}/shares/{userId} (owner only). func (s *Server) handleDeleteShare(w http.ResponseWriter, r *http.Request) { carID := s.requireCarOwner(w, r) if carID == "" { return } existing, err := s.findShare(r, carID, r.PathValue("userId")) if err != nil { writePBError(w, err) return } if existing == nil { w.WriteHeader(http.StatusNoContent) // already not shared — idempotent return } if err := s.pb.Delete(r.Context(), colShares, existing.ID); err != nil { writePBError(w, err) return } w.WriteHeader(http.StatusNoContent) } // findShare returns the car_shares grant for (carID, userID), or nil if none. func (s *Server) findShare(r *http.Request, carID, userID string) (*shareRecord, error) { res, err := s.pb.List(r.Context(), colShares, url.Values{ "filter": {fmt.Sprintf("car='%s' && user='%s'", carID, userID)}, "perPage": {"1"}, }) if err != nil { return nil, err } var recs []shareRecord if err := json.Unmarshal(res.Items, &recs); err != nil { return nil, err } if len(recs) == 0 { return nil, nil } return &recs[0], nil } // findUserByEmail looks up a user in the auth collection by email, returning nil // if none matches. func (s *Server) findUserByEmail(r *http.Request, email string) (*userRecord, error) { res, err := s.pb.List(r.Context(), s.usersCollection, url.Values{ "filter": {fmt.Sprintf("email='%s'", strings.ReplaceAll(email, "'", ""))}, "perPage": {"1"}, }) if err != nil { return nil, err } var recs []userRecord if err := json.Unmarshal(res.Items, &recs); err != nil { return nil, err } if len(recs) == 0 { return nil, nil } return &recs[0], nil }