package handlers import ( "encoding/json" "fmt" "html/template" "net/http" "net/url" "strings" "github.com/gorilla/mux" "atcr.io/pkg/appview/db" ) // DeviceCodeRequest is the request to start device authorization type DeviceCodeRequest struct { DeviceName string `json:"device_name"` } // DeviceCodeResponse is the response with user and device codes type DeviceCodeResponse struct { DeviceCode string `json:"device_code"` UserCode string `json:"user_code"` VerificationURI string `json:"verification_uri"` ExpiresIn int `json:"expires_in"` Interval int `json:"interval"` } // DeviceCodeHandler handles POST /auth/device/code type DeviceCodeHandler struct { Store *db.DeviceStore AppViewBaseURL string // e.g., "http://localhost:5000" } func (h *DeviceCodeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req DeviceCodeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request", http.StatusBadRequest) return } // Default device name if not provided if req.DeviceName == "" { req.DeviceName = "Unknown Device" } // Get client IP ip := getClientIP(r) // Get user agent userAgent := r.UserAgent() // Create pending authorization pending, err := h.Store.CreatePendingAuth(req.DeviceName, ip, userAgent) if err != nil { http.Error(w, "failed to create authorization", http.StatusInternalServerError) return } // Return device code info resp := DeviceCodeResponse{ DeviceCode: pending.DeviceCode, UserCode: pending.UserCode, VerificationURI: h.AppViewBaseURL + "/device", ExpiresIn: 600, // 10 minutes Interval: 5, // Poll every 5 seconds } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // DeviceTokenRequest is the request to poll for device authorization type DeviceTokenRequest struct { DeviceCode string `json:"device_code"` } // DeviceTokenResponse is the response with device secret or error type DeviceTokenResponse struct { DeviceSecret string `json:"device_secret,omitempty"` Handle string `json:"handle,omitempty"` DID string `json:"did,omitempty"` Error string `json:"error,omitempty"` } // DeviceTokenHandler handles POST /auth/device/token type DeviceTokenHandler struct { Store *db.DeviceStore } func (h *DeviceTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req DeviceTokenRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request", http.StatusBadRequest) return } // Get pending authorization pending, ok := h.Store.GetPendingByDeviceCode(req.DeviceCode) if !ok { resp := DeviceTokenResponse{ Error: "expired_token", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) return } // Check if approved if pending.ApprovedDID == nil || *pending.ApprovedDID == "" { // Still pending resp := DeviceTokenResponse{ Error: "authorization_pending", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) return } // Approved! Get device from store to find handle devices := h.Store.ListDevices(*pending.ApprovedDID) var handle string for _, d := range devices { if d.DID == *pending.ApprovedDID { handle = d.Handle break } } // Return device secret resp := DeviceTokenResponse{ DeviceSecret: *pending.DeviceSecret, Handle: handle, DID: *pending.ApprovedDID, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // DeviceApprovalPageHandler handles GET /device type DeviceApprovalPageHandler struct { Store *db.DeviceStore SessionStore *db.SessionStore } func (h *DeviceApprovalPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Check if user is logged in sessionID, ok := db.GetSessionID(r) if !ok { // Not logged in - redirect to login with return URL // Explicitly build the return URL with query parameters returnTo := r.URL.Path if r.URL.RawQuery != "" { returnTo = r.URL.Path + "?" + r.URL.RawQuery } http.SetCookie(w, &http.Cookie{ Name: "oauth_return_to", Value: returnTo, Path: "/", MaxAge: 600, // 10 minutes HttpOnly: true, }) http.Redirect(w, r, "/auth/oauth/login?return_to="+url.QueryEscape(returnTo), http.StatusFound) return } sess, ok := h.SessionStore.Get(sessionID) if !ok { // Invalid session - explicitly build return URL with query parameters returnTo := r.URL.Path if r.URL.RawQuery != "" { returnTo = r.URL.Path + "?" + r.URL.RawQuery } http.SetCookie(w, &http.Cookie{ Name: "oauth_return_to", Value: returnTo, Path: "/", MaxAge: 600, HttpOnly: true, }) http.Redirect(w, r, "/auth/oauth/login?return_to="+url.QueryEscape(returnTo), http.StatusFound) return } // Get user code from query userCode := r.URL.Query().Get("user_code") if userCode == "" { http.Error(w, "user_code required", http.StatusBadRequest) return } // Get pending authorization pending, ok := h.Store.GetPendingByUserCode(userCode) if !ok { h.renderError(w, "Invalid or expired authorization code") return } // Check if already approved if pending.ApprovedDID != nil && *pending.ApprovedDID != "" { h.renderSuccess(w, pending.DeviceName) return } // Render approval page h.renderApprovalPage(w, sess.Handle, pending) } // DeviceApproveRequest is the request to approve a device type DeviceApproveRequest struct { UserCode string `json:"user_code"` Approve bool `json:"approve"` } // DeviceApproveHandler handles POST /device/approve type DeviceApproveHandler struct { Store *db.DeviceStore SessionStore *db.SessionStore } func (h *DeviceApproveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Check session sessionID, ok := db.GetSessionID(r) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } sess, ok := h.SessionStore.Get(sessionID) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var req DeviceApproveRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request", http.StatusBadRequest) return } if !req.Approve { // User denied w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "denied"}) return } // Approve the device _, err := h.Store.ApprovePending(req.UserCode, sess.DID, sess.Handle) if err != nil { fmt.Printf("ERROR [device/approve]: Failed to approve: %v\n", err) http.Error(w, fmt.Sprintf("failed to approve: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "approved"}) } // ListDevicesHandler handles GET /api/devices type ListDevicesHandler struct { Store *db.DeviceStore SessionStore *db.SessionStore } func (h *ListDevicesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Check session sessionID, ok := db.GetSessionID(r) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } sess, ok := h.SessionStore.Get(sessionID) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } // Get devices for this user devices := h.Store.ListDevices(sess.DID) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(devices) } // RevokeDeviceHandler handles DELETE /api/devices/{id} type RevokeDeviceHandler struct { Store *db.DeviceStore SessionStore *db.SessionStore } func (h *RevokeDeviceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Check session sessionID, ok := db.GetSessionID(r) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } sess, ok := h.SessionStore.Get(sessionID) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } // Get device ID from URL vars := mux.Vars(r) deviceID := vars["id"] if deviceID == "" { http.Error(w, "device ID required", http.StatusBadRequest) return } // Revoke device if err := h.Store.RevokeDevice(sess.DID, deviceID); err != nil { http.Error(w, fmt.Sprintf("failed to revoke: %v", err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } // Helper functions func (h *DeviceApprovalPageHandler) renderApprovalPage(w http.ResponseWriter, handle string, pending *db.PendingAuthorization) { tmpl := template.Must(template.New("approval").Parse(deviceApprovalTemplate)) data := struct { Handle string DeviceName string UserCode string IPAddress string }{ Handle: handle, DeviceName: pending.DeviceName, UserCode: pending.UserCode, IPAddress: pending.IPAddress, } w.Header().Set("Content-Type", "text/html; charset=utf-8") tmpl.Execute(w, data) } func (h *DeviceApprovalPageHandler) renderSuccess(w http.ResponseWriter, deviceName string) { tmpl := template.Must(template.New("success").Parse(deviceSuccessTemplate)) data := struct { DeviceName string }{ DeviceName: deviceName, } w.Header().Set("Content-Type", "text/html; charset=utf-8") tmpl.Execute(w, data) } func (h *DeviceApprovalPageHandler) renderError(w http.ResponseWriter, message string) { tmpl := template.Must(template.New("error").Parse(deviceErrorTemplate)) data := struct { Message string }{ Message: message, } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusBadRequest) tmpl.Execute(w, data) } func getClientIP(r *http.Request) string { // Check X-Forwarded-For header xff := r.Header.Get("X-Forwarded-For") if xff != "" { parts := strings.Split(xff, ",") return strings.TrimSpace(parts[0]) } // Check X-Real-IP header xri := r.Header.Get("X-Real-IP") if xri != "" { return xri } // Fall back to RemoteAddr parts := strings.Split(r.RemoteAddr, ":") if len(parts) > 0 { return parts[0] } return r.RemoteAddr } // HTML templates const deviceApprovalTemplate = ` Authorize Device - ATCR

Authorize Device

User: {{.Handle}}

{{.UserCode}}
Device Name:
{{.DeviceName}}
IP Address:
{{.IPAddress}}

Do you want to authorize this device?

This device will be able to push and pull container images to your registry.

` const deviceSuccessTemplate = ` Device Authorized - ATCR

✓ Device Authorized!

Device {{.DeviceName}} has been successfully authorized.

You can now close this window and return to your terminal.

View your authorized devices

` const deviceErrorTemplate = ` Authorization Error - ATCR

✗ Authorization Error

{{.Message}}

Return to home

`