147 lines
5.0 KiB
Go
147 lines
5.0 KiB
Go
package acp
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"xworkmate-bridge/internal/shared"
|
|
)
|
|
|
|
type bridgeBootstrapConsumeRequest struct {
|
|
Ticket string `json:"ticket"`
|
|
Bridge string `json:"bridge"`
|
|
}
|
|
|
|
type accountsBridgeBootstrapConsumeResponse struct {
|
|
TicketID string `json:"ticketId"`
|
|
TargetBridge string `json:"targetBridge"`
|
|
OpenclawURL string `json:"openclawUrl"`
|
|
AuthMode string `json:"authMode"`
|
|
ExchangeToken string `json:"exchangeToken"`
|
|
ExpiresAt string `json:"expiresAt"`
|
|
Scopes []string `json:"scopes"`
|
|
}
|
|
|
|
type bridgeBootstrapResponse struct {
|
|
SetupCode string `json:"setupCode"`
|
|
BridgeOrigin string `json:"bridgeOrigin"`
|
|
AuthMode string `json:"authMode"`
|
|
ExpiresAt string `json:"expiresAt"`
|
|
IssuedBy string `json:"issuedBy"`
|
|
Scopes []string `json:"scopes"`
|
|
}
|
|
|
|
func (s *Server) HandleBridgeBootstrapHealth(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"ok": true,
|
|
"bridgeOrigin": bridgePublicBaseURL(),
|
|
"issuedBy": "xworkmate-bridge",
|
|
})
|
|
}
|
|
|
|
func (s *Server) HandleBridgeBootstrapConsume(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
var req bridgeBootstrapConsumeRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.Ticket = strings.TrimSpace(req.Ticket)
|
|
req.Bridge = strings.TrimSpace(req.Bridge)
|
|
if req.Ticket == "" {
|
|
http.Error(w, "ticket is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.Bridge == "" {
|
|
req.Bridge = bridgePublicBaseURL()
|
|
}
|
|
|
|
payload, status, err := consumeBootstrapFromAccounts(req)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), status)
|
|
return
|
|
}
|
|
|
|
setupCodePayload := map[string]any{
|
|
"url": payload.OpenclawURL,
|
|
"token": payload.ExchangeToken,
|
|
"exchangeToken": payload.ExchangeToken,
|
|
"authMode": payload.AuthMode,
|
|
"expiresAt": payload.ExpiresAt,
|
|
"bridgeOrigin": req.Bridge,
|
|
"issuedBy": "xworkmate-bridge",
|
|
}
|
|
setupCodeBytes, err := json.Marshal(setupCodePayload)
|
|
if err != nil {
|
|
http.Error(w, "failed to encode setup payload", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(bridgeBootstrapResponse{
|
|
SetupCode: string(setupCodeBytes),
|
|
BridgeOrigin: req.Bridge,
|
|
AuthMode: payload.AuthMode,
|
|
ExpiresAt: payload.ExpiresAt,
|
|
IssuedBy: "xworkmate-bridge",
|
|
Scopes: append([]string(nil), payload.Scopes...),
|
|
})
|
|
}
|
|
|
|
func consumeBootstrapFromAccounts(req bridgeBootstrapConsumeRequest) (accountsBridgeBootstrapConsumeResponse, int, error) {
|
|
baseURL := strings.TrimRight(strings.TrimSpace(shared.EnvOrDefault("ACCOUNTS_BASE_URL", "https://accounts.svc.plus")), "/")
|
|
if baseURL == "" {
|
|
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("accounts base url is not configured")
|
|
}
|
|
serviceToken := strings.TrimSpace(shared.EnvOrDefault("INTERNAL_SERVICE_TOKEN", ""))
|
|
if serviceToken == "" {
|
|
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("internal service token is not configured")
|
|
}
|
|
body, err := json.Marshal(req)
|
|
if err != nil {
|
|
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("failed to encode consume request")
|
|
}
|
|
httpReq, err := http.NewRequest(http.MethodPost, baseURL+"/api/internal/xworkmate/bridge/bootstrap/consume", bytes.NewReader(body))
|
|
if err != nil {
|
|
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("failed to create consume request")
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
httpReq.Header.Set("X-Service-Token", serviceToken)
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(httpReq)
|
|
if err != nil {
|
|
return accountsBridgeBootstrapConsumeResponse{}, http.StatusBadGateway, fmt.Errorf("failed to contact accounts service")
|
|
}
|
|
defer func() {
|
|
_ = resp.Body.Close()
|
|
}()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return accountsBridgeBootstrapConsumeResponse{}, resp.StatusCode, fmt.Errorf("accounts bootstrap consume failed")
|
|
}
|
|
var payload accountsBridgeBootstrapConsumeResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
|
return accountsBridgeBootstrapConsumeResponse{}, http.StatusBadGateway, fmt.Errorf("failed to decode accounts bootstrap response")
|
|
}
|
|
return payload, http.StatusOK, nil
|
|
}
|
|
|
|
func bridgePublicBaseURL() string {
|
|
value := strings.TrimSpace(shared.EnvOrDefault("BRIDGE_PUBLIC_BASE_URL", "https://xworkmate-bridge.svc.plus"))
|
|
if value == "" {
|
|
return "https://xworkmate-bridge.svc.plus"
|
|
}
|
|
return strings.TrimRight(value, "/")
|
|
}
|