package devices import ( "encoding/json" "net/http" "strings" "github.com/go-chi/chi/v5" "github.com/wangjia/pangolin/server/internal/apierr" "github.com/wangjia/pangolin/server/internal/auth" ) // Handler serves the account device endpoints. It assumes the JWT auth // middleware (module #2) has set CtxKeyUserID on the request context. type Handler struct { svc *Service } // NewHandler creates a Handler backed by svc. func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } // RegisterRoutes mounts the device routes on r under the caller's chosen prefix // (the API mounts these beneath /v1/me): // // GET /devices // DELETE /devices/{id} // POST /devices/{id}/logout func (h *Handler) RegisterRoutes(r chi.Router) { r.Get("/devices", h.ListDevices) r.Delete("/devices/{id}", h.DeleteDevice) r.Post("/devices/{id}/logout", h.ForceLogout) r.Post("/devices/{id}/rename", h.RenameDevice) r.Get("/session", h.SessionCheck) // GET /v1/me/session?device_id= — 会话有效性轮询 } type listDevicesResponse struct { Devices []Device `json:"devices"` } // ListDevices handles GET /v1/me/devices. func (h *Handler) ListDevices(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserIDFromContext(r.Context()) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return } devices, apiErr := h.svc.ListDevices(r.Context(), userID) if apiErr != nil { apierr.WriteJSON(w, StatusForError(apiErr), apiErr) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(listDevicesResponse{Devices: devices}) } // DeleteDevice handles DELETE /v1/me/devices/{id}. func (h *Handler) DeleteDevice(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserIDFromContext(r.Context()) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return } deviceUUID := chi.URLParam(r, "id") if apiErr := h.svc.DeleteDevice(r.Context(), userID, deviceUUID); apiErr != nil { apierr.WriteJSON(w, StatusForError(apiErr), apiErr) return } w.WriteHeader(http.StatusNoContent) } // ForceLogout handles POST /v1/me/devices/{id}/logout — revoke the device's // sessions (kick it offline); the device stays in the list. func (h *Handler) ForceLogout(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserIDFromContext(r.Context()) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return } deviceUUID := chi.URLParam(r, "id") if apiErr := h.svc.ForceLogout(r.Context(), userID, deviceUUID); apiErr != nil { apierr.WriteJSON(w, StatusForError(apiErr), apiErr) return } w.WriteHeader(http.StatusNoContent) } // SessionCheck handles GET /v1/me/session?device_id=UUID — reports whether the // caller's device still has a live session. false ⇒ force-logged-out elsewhere; // the client polls this (~15s) and logs out on {"active":false} → near-instant kick. // Always 200 (the verdict is in the body); device_id 取自 query。 func (h *Handler) SessionCheck(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserIDFromContext(r.Context()) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return } deviceUUID := strings.TrimSpace(r.URL.Query().Get("device_id")) active, apiErr := h.svc.SessionActive(r.Context(), userID, deviceUUID) if apiErr != nil { apierr.WriteJSON(w, StatusForError(apiErr), apiErr) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(map[string]bool{"active": active}) } // RenameDevice handles POST /v1/me/devices/{id}/rename — set a device's name. func (h *Handler) RenameDevice(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserIDFromContext(r.Context()) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return } var req struct { Name string `json:"name"` } if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<14)).Decode(&req); err != nil { apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest) return } deviceUUID := chi.URLParam(r, "id") if apiErr := h.svc.RenameDevice(r.Context(), userID, deviceUUID, req.Name); apiErr != nil { apierr.WriteJSON(w, StatusForError(apiErr), apiErr) return } w.WriteHeader(http.StatusNoContent) }