package api import ( "context" "encoding/json" "net/http" "time" ) type loginReq struct { Username string `json:"username"` Password string `json:"password"` // Repo is an optional override: validate creds against a specific // repo on a specific agent. If empty, we use the gateway's default // repo (api-gateway) on the connected gateway agent. Repo string `json:"repo"` } func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } var body loginReq if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } if body.Username == "" || body.Password == "" { writeErr(w, http.StatusBadRequest, "username and password required") return } ok := s.validateViaAgent(r.Context(), body.Username, body.Password, body.Repo) if !ok { writeErr(w, http.StatusUnauthorized, "login failed — git ls-remote rejected the credentials") return } tok := s.sess.Issue(body.Username) http.SetCookie(w, &http.Cookie{ Name: "sdp_session", Value: tok, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: 12 * 3600, }) writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) } func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { if c, err := r.Cookie("sdp_session"); err == nil { s.sess.Revoke(c.Value) } http.SetCookie(w, &http.Cookie{ Name: "sdp_session", Value: "", Path: "/", HttpOnly: true, MaxAge: -1, }) writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) } // validateViaAgent asks the connected gateway agent to run // `git ls-remote` against the api-gateway repo. The agent holds the // repo and the trust boundary for Bitbucket creds. func (s *Server) validateViaAgent(ctx context.Context, user, pass, repo string) bool { s.agents.mu.RLock() _, connected := s.agents.conns["gateway"] s.agents.mu.RUnlock() if !connected { return false } if repo == "" { repo = s.gatewayRepo } data := map[string]string{ "repo": repo, "username": user, "password": pass, } ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() raw, err := s.hub.CallAgent(ctx, "gateway", "probe", data, 0) if err != nil { return false } var reply struct { OK bool `json:"ok"` Err string `json:"error,omitempty"` } if err := json.Unmarshal(raw, &reply); err != nil { return false } return reply.OK }