From b04ee59bf725d17ddb8d619fba6876510260f1d7 Mon Sep 17 00:00:00 2001 From: Daniel Apatin Date: Thu, 21 Aug 2025 08:07:07 +0000 Subject: [PATCH] Refactor handlers and introduce API wrapper - Updated Handler struct to include an API wrapper for better abstraction. - Added middleware for JSON error handling. - Refactored Operators and Places handlers to utilize the new API wrapper. - Simplified Snapshot and Stream handlers to use the API wrapper for upstream requests. - Removed deprecated token handling logic from handlers. - Introduced centralized models for accounts, confirmations, cameras, places, and finances. - Created a new upstream request handling mechanism for better error management. - Implemented token refresh logic with a dedicated auth package. - Added constants for API host and user agent. - Enhanced logging capabilities for upstream requests. --- README.md | 2 +- config.json | 2 +- config/config.go | 10 + handlers/api.go | 4 +- handlers/auth.go | 333 ++++---------------------------- handlers/cameras.go | 86 ++------- handlers/const.go | 131 ------------- handlers/door.go | 99 +++------- handlers/events.go | 223 ++++----------------- handlers/finances.go | 103 ++-------- handlers/handlers.go | 5 +- handlers/middleware.go | 32 +++ handlers/operators.go | 72 ++----- handlers/places.go | 84 ++------ handlers/snapshot.go | 77 +++----- handlers/stream.go | 103 ++-------- handlers/token.go | 68 ------- handlers/types.go | 45 +++++ internal/api/api.go | 168 ++++++++++++++++ internal/api/auth.go | 68 +++++++ internal/api/errors.go | 55 ++++++ internal/auth/client.go | 76 ++++++++ internal/auth/providers.go | 11 ++ internal/auth/refresh.go | 63 ++++++ internal/constants/constants.go | 7 + internal/models/models.go | 111 +++++++++++ internal/upstream/request.go | 119 ++++++++++++ main.go | 29 ++- 28 files changed, 1000 insertions(+), 1186 deletions(-) delete mode 100644 handlers/const.go create mode 100644 handlers/middleware.go delete mode 100644 handlers/token.go create mode 100644 handlers/types.go create mode 100644 internal/api/api.go create mode 100644 internal/api/auth.go create mode 100644 internal/api/errors.go create mode 100644 internal/auth/client.go create mode 100644 internal/auth/providers.go create mode 100644 internal/auth/refresh.go create mode 100644 internal/constants/constants.go create mode 100644 internal/models/models.go create mode 100644 internal/upstream/request.go diff --git a/README.md b/README.md index 86bde07..3609170 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Feel free to **file a new issue** with a respective title and description on the ## ✅  Requirements -Requires a **Go version higher or equal to 1.11**. +Requires a **Go version higher or equal to 1.24**. ## 📘  License diff --git a/config.json b/config.json index d51ee02..acacdfc 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "name": "Domofon", - "version": "0.3.6", + "version": "0.3.7", "slug": "domofon", "description": "", "startup": "application", diff --git a/config/config.go b/config/config.go index 2145b49..07972b8 100644 --- a/config/config.go +++ b/config/config.go @@ -23,6 +23,16 @@ type Config struct { UUID string `json:"uuid"` } +// Interface helpers for new auth layer +func (c *Config) GetRefreshToken() string { return c.RefreshToken } +func (c *Config) GetOperatorID() int { return c.Operator } +func (c *Config) GetUUID() string { return c.UUID } +func (c *Config) SetTokens(access, refresh string) error { + c.Token = access + c.RefreshToken = refresh + return c.WriteConfig() +} + // InitConfig ... func InitConfig() *Config { config := &Config{Port: 18000} diff --git a/handlers/api.go b/handlers/api.go index 13406b4..bc47cf5 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -10,6 +10,8 @@ import ( "os" "strings" "time" + + "github.com/ad/domru/internal/constants" ) // HANetwork ... @@ -28,7 +30,7 @@ func (h *Handler) HANetwork() (string, error) { return "", fmt.Errorf("supervisor token not set") } - url := API_HA_NETWORK + url := constants.HANetworkInfoURL request, err := http.NewRequest("GET", url, nil) if err != nil { diff --git a/handlers/auth.go b/handlers/auth.go index 86b8ec7..42e89eb 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -1,15 +1,12 @@ package handlers import ( - "bytes" - "context" "encoding/json" "fmt" "html/template" "log" "net/http" "strconv" - "time" ) // LoginHandler ... @@ -24,10 +21,12 @@ func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { if err := r.ParseForm(); err != nil { - loginError = fmt.Sprintf("ParseForm() err: %v", err) + log.Printf("ParseForm() err: %v", err) + loginError = "parse form error" } else { phone := r.FormValue("phone") - accounts, err := h.Accounts(&phone) + // use new API layer if possible + accounts, err := h.API.Accounts(r.Context(), phone) if err != nil { loginError = fmt.Sprintf("login error: %v", err.Error()) } else { @@ -68,33 +67,8 @@ func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { return } } - - var tmpl []byte - var err error - if tmpl, err = h.TemplateFs.ReadFile("templates/login.html"); err != nil { - fmt.Println(err) - } - - data := LoginPageData{loginError, strconv.Itoa(h.Config.Login), ingressPath} - - t := template.New("t") - t, err = t.Parse(string(tmpl)) - if err != nil { - loginError = err.Error() - } else { - err = t.Execute(w, data) - if err != nil { - loginError = err.Error() - } - } - - if loginError != "" { - log.Println(loginError) - } } - -// LoginAddressHandler ... -func (h *Handler) LoginAddressHandler(w http.ResponseWriter, r *http.Request) { +func (h *Handler) LoginAddressHandler(w http.ResponseWriter, r *http.Request) { // still transitional, uses API wrapper ingressPath := r.Header.Get("X-Ingress-Path") // log.Println(r.Method, "/login/address", ingressPath) @@ -117,24 +91,18 @@ func (h *Handler) LoginAddressHandler(w http.ResponseWriter, r *http.Request) { } else { account := h.UserAccounts[accountIndex] h.Account = &account - result, err := h.RequestCode(&phone, account) + err := h.API.RequestCode(r.Context(), phone, account) if err != nil { loginError = fmt.Sprintf("loginAddress error: %v", err.Error()) } - if n, err := strconv.Atoi(phone); err == nil { h.Config.Login = n } - h.Config.Operator = int(h.Account.OperatorID) if err = h.Config.WriteConfig(); err != nil { log.Println("error on write config file ", err) } - if !result && loginError == "" { - loginError = "Error on sms send" - } - } } @@ -189,32 +157,29 @@ func (h *Handler) HomeHandler(w http.ResponseWriter, r *http.Request) { } var cameras Cameras - - if loginError == "" { - camerasData, err := h.Cameras() - if err != nil { - loginError = "cameras (" + camerasData + ") got " + err.Error() + var places Places + var finances Finances + if h.API != nil && loginError == "" { + if cams, err := h.API.Cameras(r.Context()); err != nil { + loginError = "cameras api error: " + err.Error() } else { - if err := json.Unmarshal([]byte(camerasData), &cameras); err != nil { - loginError = "cameras (" + camerasData + ") Unmarshal got " + err.Error() - } + cameras = cams } } - - var places Places - - if loginError == "" { - placesData, err := h.Places() - if err != nil { - loginError = "places (" + placesData + ") got " + err.Error() + if h.API != nil && loginError == "" { + if pls, err := h.API.Places(r.Context()); err != nil { + loginError = "places api error: " + err.Error() } else { - if err := json.Unmarshal([]byte(placesData), &places); err != nil { - loginError = "places (" + placesData + ") Unmarshal got " + err.Error() - } + places = pls + } + } + if h.API != nil && loginError == "" { + if fin, err := h.API.Finances(r.Context()); err != nil { + loginError = "finances api error: " + err.Error() + } else { + finances = fin } } - - finances, _ := h.GetFinances() data := HomePageData{ HassioIngress: ingressPath, @@ -226,7 +191,7 @@ func (h *Handler) HomeHandler(w http.ResponseWriter, r *http.Request) { RefreshToken: h.Config.RefreshToken, Cameras: cameras, Places: places, - Finances: *finances, + Finances: finances, } var tmpl []byte @@ -251,256 +216,32 @@ func (h *Handler) HomeHandler(w http.ResponseWriter, r *http.Request) { } } -// Accounts ... -func (h *Handler) Accounts(username *string) (a []Account, err error) { - var ( - body []byte - client = http.DefaultClient - ) - - url := fmt.Sprintf(API_AUTH_LOGIN, *username) - // log.Println("/accountsHandler", url) - - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - request = request.WithContext(ctx) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json") - rt.Set("Connection", "keep-alive") - rt.Set("Accept", "*/*") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, 0)) - rt.Set("Authorization", "") - rt.Set("Accept-Language", "en-us") - rt.Set("Accept-Encoding", "gzip, deflate, br") - - client.Transport = rt - - resp, err := client.Do(request) - if err != nil { - return nil, err - } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) - } - }() - - if resp.StatusCode == 409 { // Conflict (tokent already expired) - return nil, fmt.Errorf("token can't be refreshed") - } - - if body, err = ReadResponseBody(resp); err != nil { - return nil, err - } - - var accounts []Account - if err = json.Unmarshal(body, &accounts); err != nil { - return nil, err - } - - return accounts, nil -} - -// RequestCode ... -func (h *Handler) RequestCode(username *string, account Account) (result bool, err error) { - var ( - body []byte - client = http.DefaultClient - ) - - url := fmt.Sprintf(API_AUTH_CONFIRMATION, *username) - // log.Println("/requestCodeHandler", url) - - b, err := json.Marshal(account) - if err != nil { - return false, err - } - - request, err := http.NewRequest("POST", url, bytes.NewBuffer(b)) - if err != nil { - return false, err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - request = request.WithContext(ctx) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, account.PlaceID)) - rt.Set("Connection", "keep-alive") - rt.Set("Accept", "*/*") - rt.Set("Accept-Language", "en-us") - rt.Set("Accept-Encoding", "gzip, deflate, br") - rt.Set("Authorization", "") - - client.Transport = rt - - resp, err := client.Do(request) - if err != nil { - return false, err - } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) - } - }() - - if resp.StatusCode == 409 { // Conflict (tokent already expired) - return false, fmt.Errorf("token can't be refreshed") - } - - if resp.StatusCode == 200 { - return true, nil - } - - if body, err = ReadResponseBody(resp); err != nil { - return false, err - } - - return false, fmt.Errorf("status %d\n%s", resp.StatusCode, body) -} - -// SendCode ... -func (h *Handler) SendCode(r *http.Request) (authToken, refreshToken string, err error) { - var ( - body []byte - client = http.DefaultClient - ) - - url := fmt.Sprintf(API_AUTH_CONFIRMATION_SMS, strconv.Itoa(h.Config.Login)) - - if err := r.ParseForm(); err != nil { - return "", "", fmt.Errorf("ParseForm() err: %v", err) - } - - code := r.FormValue("code") - - c := ConfirmRequest{ - Confirm: code, - SubscriberID: h.Account.SubscriberID, - Login: strconv.Itoa(h.Config.Login), - OperatorID: int64(h.Config.Operator), - AccountID: h.Account.AccountID, - ProfileID: h.Account.ProfileID, - } - - b, err := json.Marshal(c) - if err != nil { - return "", "", err - } - - // log.Println("/sms", url, string(b)) - - request, err := http.NewRequest("POST", url, bytes.NewBuffer(b)) - if err != nil { - return "", "", err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - request = request.WithContext(ctx) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, h.Account.PlaceID)) - rt.Set("Connection", "keep-alive") - rt.Set("Accept", "*/*") - rt.Set("Accept-Language", "en-us") - rt.Set("Accept-Encoding", "gzip, deflate, br") - rt.Set("Authorization", "") - - client.Transport = rt - - resp, err := client.Do(request) - if err != nil { - return "", "", err - } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) - } - }() - - if resp.StatusCode == 409 { // Conflict (tokent already expired) - return "", "", fmt.Errorf("token can't be refreshed") - } - - if body, err = ReadResponseBody(resp); err != nil { - return "", "", err - } - - if resp.StatusCode == 200 { - var authResp ConfirmResponse - if err = json.Unmarshal(body, &authResp); err != nil { - return "", "", err - } - - return authResp.AccessToken, authResp.RefreshToken, nil - } - - return "", "", fmt.Errorf("unknown error with status %d\n%s", resp.StatusCode, body) -} - // AccountsHandler ... func (h *Handler) AccountsHandler(w http.ResponseWriter, r *http.Request) { - // log.Println("/accountsHandler") - - login := strconv.Itoa(h.Config.Login) - - data, err := h.Accounts(&login) - if err != nil { - log.Println("accountsHandler", err.Error()) - } - w.Header().Set("Content-Type", "application/json") - - b, err := json.Marshal(data) + data, err := h.API.Accounts(r.Context(), strconv.Itoa(h.Config.Login)) if err != nil { - fmt.Printf("Error: %s", err) - + writeJSONError(w, err) return } - - if _, err := w.Write(b); err != nil { - log.Println("accountsHandler", err.Error()) + if b, e := json.Marshal(data); e == nil { + w.Write(b) + } else { + writeJSONError(w, e) } } // LoginSMSHandler ... func (h *Handler) LoginSMSHandler(w http.ResponseWriter, r *http.Request) { - // log.Println("/sms") - - access, refresh, err := h.SendCode(r) + w.Header().Set("Content-Type", "application/json") + access, refresh, err := h.API.ConfirmCode(r.Context(), strconv.Itoa(h.Config.Login), r.FormValue("code"), *h.Account) if err != nil { - log.Println("sms", err.Error()) + writeJSONError(w, err) + return } - h.Config.Token = access h.Config.RefreshToken = refresh - if err = h.Config.WriteConfig(); err != nil { - log.Println("error on write config file ", err) - } - - if _, err := w.Write([]byte(access + " / " + refresh)); err != nil { - log.Println("sms", err.Error()) - } + _ = h.Config.WriteConfig() + b, _ := json.Marshal(map[string]string{"accessToken": access, "refreshToken": refresh}) + w.Write(b) } diff --git a/handlers/cameras.go b/handlers/cameras.go index 6ced649..a5c91a6 100644 --- a/handlers/cameras.go +++ b/handlers/cameras.go @@ -1,81 +1,33 @@ package handlers import ( - "context" + "encoding/json" + "errors" "log" "net/http" - "strconv" - "time" -) -// Cameras ... -func (h *Handler) Cameras() (string, error) { - var ( - body []byte - err error - client = http.DefaultClient - ) + "github.com/ad/domru/internal/api" +) - url := API_CAMERAS +var ErrAPINotInitialized = errors.New("api wrapper not initialized") - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", err +// CamerasHandler uses API wrapper to return cameras list. +func (h *Handler) CamerasHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if h.API == nil { + writeJSONError(w, ErrAPINotInitialized) + return } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - request = request.WithContext(ctx) - - operator := strconv.Itoa(h.Config.Operator) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json; charset=UTF-8") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, 0)) - rt.Set("Operator", operator) - rt.Set("Authorization", "Bearer "+h.Config.Token) - client.Transport = rt - - resp, err := client.Do(request) + cams, err := h.API.Cameras(r.Context()) if err != nil { - log.Printf("%+v %s %s", resp, operator, h.Config.Token) - return "", err - } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) + if se := api.StatusFromError(err); se != http.StatusBadGateway { + w.WriteHeader(se) } - }() - - // log.Printf("%+v %s %s", resp, operator, h.Config.Token) - - if resp.StatusCode == 409 { // Conflict (tokent already expired) - return "token can't be refreshed", nil - } - - if body, err = ReadResponseBody(resp); err != nil { - return "", err + writeJSONError(w, err) + return } - - return string(body), nil -} - -// CamerasHandler ... -func (h *Handler) CamerasHandler(w http.ResponseWriter, r *http.Request) { - // log.Println("/camerasHandler") - - data, err := h.Cameras() - if err != nil { - log.Println("camerasHandler", err.Error()) - } - - w.Header().Set("Content-Type", "application/json") - - if _, err := w.Write([]byte(data)); err != nil { - log.Println("camerasHandler", err.Error()) + b, _ := json.Marshal(cams) + if _, err := w.Write(b); err != nil { + log.Println("camerasHandler write error", err) } } diff --git a/handlers/const.go b/handlers/const.go deleted file mode 100644 index 4171890..0000000 --- a/handlers/const.go +++ /dev/null @@ -1,131 +0,0 @@ -package handlers - -import "fmt" - -const ( - API_HOST = "myhome.proptech.ru" - - API_HA_NETWORK = "http://supervisor/network/info" - - API_AUTH_LOGIN = "https://myhome.proptech.ru/auth/v2/login/%s" - API_AUTH_CONFIRMATION = "https://myhome.proptech.ru/auth/v2/confirmation/%s" - API_AUTH_CONFIRMATION_SMS = "https://myhome.proptech.ru/auth/v2/auth/%s/confirmation" - - API_CAMERAS = "https://myhome.proptech.ru/rest/v1/forpost/cameras" - API_OPEN_DOOR = "https://myhome.proptech.ru/rest/v1/places/%s/accesscontrols/%s/actions" - API_FINANCES = "https://myhome.proptech.ru/rest/v1/subscribers/profiles/finances" - API_SUBSCRIBER_PLACES = "https://myhome.proptech.ru/rest/v1/subscriberplaces" - API_VIDEO_SNAPSHOT = "https://myhome.proptech.ru/rest/v1/places/%s/accesscontrols/%s/videosnapshots" - API_CAMERA_GET_STREAM = "https://myhome.proptech.ru/rest/v1/forpost/cameras/%s/video" - API_REFRESH_SESSION = "https://myhome.proptech.ru/auth/v2/session/refresh" - API_EVENTS = "https://myhome.proptech.ru/rest/v1/places/%s/events?allowExtentedActions=true" - API_OPERATORS = "https://myhome.proptech.ru/public/v1/operators" -) - -// GenerateUserAgent создает User-Agent с operatorID, UUID и placeID -func GenerateUserAgent(operatorID int, uuid string, placeID int64) string { - // Если placeID не указан или равен 0, используем 1 по умолчанию - if placeID == 0 { - placeID = 1 - } - return fmt.Sprintf("Google sdkgphone64x8664 | Android 14 | erth | 8.26.0 (82600010) | | %d | %s | %d", operatorID, uuid, placeID) -} - -type Account struct { - OperatorID int64 `json:"operatorId"` - SubscriberID int64 `json:"subscriberId"` - AccountID string `json:"accountId"` - PlaceID int64 `json:"placeId"` - Address string `json:"address"` - ProfileID string `json:"profileId"` -} - -type ConfirmRequest struct { - Confirm string `json:"confirm1"` - SubscriberID int64 `json:"subscriberId"` - Login string `json:"login"` - OperatorID int64 `json:"operatorId"` - AccountID string `json:"accountId"` - ProfileID string `json:"profileId"` -} - -type ConfirmResponse struct { - OperatorID int64 `json:"operatorId"` - TokenType string `json:"tokenType"` - AccessToken string `json:"accessToken"` - RefreshToken string `json:"refreshToken"` -} - -type AccountsPageData struct { - Accounts []Account - Phone string - HassioIngress string - LoginError string -} - -type LoginPageData struct { - LoginError string - Phone string - HassioIngress string -} - -type SMSPageData struct { - Phone string - Index string - HassioIngress string - LoginError string -} - -type Places struct { - Data []struct { - ID int `json:"id"` - Place struct { - ID int `json:"id"` - Address struct { - VisibleAddress string `json:"visibleAddress"` - } `json:"address"` - AccessControls []struct { - ID int `json:"id"` - Name string `json:"name"` - } `json:"accessControls"` - } `json:"place"` - Subscriber struct { - ID int `json:"id"` - Name string `json:"name"` - AccountID string `json:"accountId"` - } `json:"subscriber"` - Blocked bool `json:"blocked"` - } `json:"data"` -} - -type Cameras struct { - Data []struct { - ID int `json:"ID"` - Name string `json:"Name"` - IsActive int `json:"IsActive"` - } `json:"data"` -} - -type HomePageData struct { - HassioIngress string - HostIP string - Port string - LoginError string - Phone string - Token string - RefreshToken string - Cameras Cameras - Places Places - Finances Finances -} - -type HAConfig struct { - Result string `json:"result"` - Data struct { - Interfaces []struct { - Ipv4 struct { - Address []string `json:"address"` - } `json:"ipv4"` - } `json:"interfaces"` - } `json:"data"` -} diff --git a/handlers/door.go b/handlers/door.go index ef00ee4..c7b03df 100644 --- a/handlers/door.go +++ b/handlers/door.go @@ -1,100 +1,45 @@ package handlers import ( - "bytes" - "context" - "encoding/json" "fmt" "log" "net/http" - "strconv" - "time" + + "github.com/ad/domru/internal/api" ) -// Door ... +// Door now uses API.OpenDoor func (h *Handler) Door(r *http.Request) (string, error) { - var ( - body []byte - err error - client = http.DefaultClient - ) - - type doorData struct { - Name string `json:"name"` + q := r.URL.Query() + placeID := q.Get("placeID") + accessControlID := q.Get("accessControlID") + if placeID == "" || accessControlID == "" { + return "", fmt.Errorf("provide placeID and accessControlID") } - - buf := new(bytes.Buffer) - if err = json.NewEncoder(buf).Encode(&doorData{Name: "accessControlOpen"}); err != nil { - return "", err + if h.API == nil { + return "", fmt.Errorf("api not initialized") } - - query := r.URL.Query() - placeID := query.Get("placeID") - accessControlID := query.Get("accessControlID") - - url := fmt.Sprintf(API_OPEN_DOOR, placeID, accessControlID) - - request, err := http.NewRequest("POST", url, buf) - if err != nil { - return "", err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - request = request.WithContext(ctx) - - operator := strconv.Itoa(h.Config.Operator) - - // Конвертируем placeID из строки в int64 - placeIDInt, _ := strconv.ParseInt(placeID, 10, 64) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json; charset=UTF-8") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, placeIDInt)) - rt.Set("Operator", operator) - rt.Set("Authorization", "Bearer "+h.Config.Token) - client.Transport = rt - - resp, err := client.Do(request) - if err != nil { + if err := h.API.OpenDoor(r.Context(), placeID, accessControlID); err != nil { return "", err } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) - } - }() - - // log.Printf("%#v", resp) - - if resp.StatusCode == 409 { // Conflict (tokent already expired) - return "token can't be refreshed", nil - } - - if body, err = ReadResponseBody(resp); err != nil { - return "", err - } - - return string(body), nil + return `{"status":"ok"}`, nil } // DoorHandler ... func (h *Handler) DoorHandler(w http.ResponseWriter, r *http.Request) { - // log.Println("/doorHandler") - + w.Header().Set("Content-Type", "application/json") data, err := h.Door(r) if err != nil { - data = err.Error() - log.Println("doorHandler", err.Error()) + if se := api.StatusFromError(err); se != http.StatusBadGateway { + w.WriteHeader(se) + } + writeJSONError(w, err) + return } - - w.Header().Set("Content-Type", "application/json") - if _, err := w.Write([]byte(data)); err != nil { - log.Println("doorHandler", err.Error()) + log.Println("doorHandler write error", err) } } + +// APIClient helper ensures we have an http.Client implementing Do. +// Legacy APIClient helpers removed (handled by API layer) diff --git a/handlers/events.go b/handlers/events.go index 76bfc46..dd8105a 100644 --- a/handlers/events.go +++ b/handlers/events.go @@ -1,208 +1,67 @@ package handlers import ( - "context" "encoding/json" "fmt" "log" "net/http" - "strconv" - "time" -) - -type EventsInputModel struct { - Data []struct { - ID string `json:"id,omitempty"` - PlaceID int `json:"placeId,omitempty"` - EventTypeName string `json:"eventTypeName,omitempty"` - Timestamp string `json:"timestamp,omitempty"` - Message string `json:"message,omitempty"` - Source struct { - Type string `json:"type,omitempty"` - ID int `json:"id,omitempty"` - } `json:"source,omitempty"` - Value struct { - Type string `json:"type,omitempty"` - Value bool `json:"value,omitempty"` - } `json:"value,omitempty"` - EventStatusValue interface{} `json:"eventStatusValue,omitempty"` - Actions []interface{} `json:"actions,omitempty"` - } `json:"data,omitempty"` -} - -// Events ... -func (h *Handler) Events(w http.ResponseWriter, r *http.Request) (string, error) { - var ( - body []byte - err error - client = http.DefaultClient - ) - - query := r.URL.Query() - placeID := query.Get("placeID") - - if placeID == "" { - return "provide placeID", fmt.Errorf("%s", "provide placeID") - } - - url := fmt.Sprintf(API_EVENTS, placeID) - // log.Println("/eventsHandler", url) - - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - request = request.WithContext(ctx) - - operator := strconv.Itoa(h.Config.Operator) - - // Конвертируем placeID из строки в int64 - placeIDInt, _ := strconv.ParseInt(placeID, 10, 64) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json; charset=UTF-8") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, placeIDInt)) - rt.Set("Operator", operator) - rt.Set("Authorization", "Bearer "+h.Config.Token) - client.Transport = rt - - resp, err := client.Do(request) - if err != nil { - return "", err - } - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) - } - }() - - // log.Printf("%#v", resp) - - if resp.StatusCode == 409 { // Conflict (tokent already expired) - return "token can't be refreshed", nil - } + "github.com/ad/domru/internal/api" +) - if body, err = ReadResponseBody(resp); err != nil { - return "", err +// EventsHandler now uses API wrapper; expects placeID query param. +func (h *Handler) EventsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if h.API == nil { + writeJSONError(w, ErrAPINotInitialized) + return } - - return string(body), nil -} - -// Events ... -func (h *Handler) LastEvent(w http.ResponseWriter, r *http.Request) (events EventsInputModel, err error) { - var ( - body []byte - client = http.DefaultClient - ) - - query := r.URL.Query() - placeID := query.Get("placeID") - + placeID := r.URL.Query().Get("placeID") if placeID == "" { - return events, fmt.Errorf("%s", "provide placeID") + writeJSONError(w, fmt.Errorf("provide placeID")) + return } - - url := fmt.Sprintf(API_EVENTS, placeID) - // log.Println("/eventsHandler", url) - - request, err := http.NewRequest("GET", url, nil) + events, err := h.API.Events(r.Context(), placeID) if err != nil { - return events, err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - request = request.WithContext(ctx) - - operator := strconv.Itoa(h.Config.Operator) - - // Конвертируем placeID из строки в int64 - placeIDInt, _ := strconv.ParseInt(placeID, 10, 64) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json; charset=UTF-8") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, placeIDInt)) - rt.Set("Operator", operator) - rt.Set("Authorization", "Bearer "+h.Config.Token) - client.Transport = rt - - resp, err := client.Do(request) - if err != nil { - return events, err - } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) + if se := api.StatusFromError(err); se != http.StatusBadGateway { + w.WriteHeader(se) } - }() - - // log.Printf("%#v", resp) - - if resp.StatusCode == 409 { // Conflict (tokent already expired) - return events, fmt.Errorf("%s", "token can't be refreshed") - } - - if body, err = ReadResponseBody(resp); err != nil { - return events, err + writeJSONError(w, err) + return } - - if err := json.Unmarshal(body, &events); err != nil { - return events, fmt.Errorf("json parse error: %q", err) - + b, _ := json.Marshal(events) + if _, err := w.Write(b); err != nil { + log.Println("eventsHandler write", err) } - - return events, nil } -// EventsHandler ... -func (h *Handler) EventsHandler(w http.ResponseWriter, r *http.Request) { - data, err := h.Events(w, r) - if err != nil { - log.Println("eventsHandler", err.Error()) - } - +// LastEventHandler returns only the latest event item when available. +func (h *Handler) LastEventHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - - if _, err := w.Write([]byte(data)); err != nil { - log.Println("eventsHandler", err.Error()) + if h.API == nil { + writeJSONError(w, ErrAPINotInitialized) + return } -} - -// EventsHandler ... -func (h *Handler) LastEventHandler(w http.ResponseWriter, r *http.Request) { - data, err := h.LastEvent(w, r) - if err != nil { - log.Println("lastEventHandler", err.Error()) + placeID := r.URL.Query().Get("placeID") + if placeID == "" { + writeJSONError(w, fmt.Errorf("provide placeID")) + return } - - w.Header().Set("Content-Type", "application/json") - - if len(data.Data) > 0 { - b, err := json.Marshal(data.Data[0]) - if err != nil { - log.Println("lastEventHandler", err.Error()) - } - - if _, err := w.Write(b); err != nil { - log.Println("lastEventHandler", err.Error()) + events, err := h.API.Events(r.Context(), placeID) + if err != nil { + if se := api.StatusFromError(err); se != http.StatusBadGateway { + w.WriteHeader(se) } - + writeJSONError(w, err) return } - - if _, err := w.Write([]byte(`{"error": "events not found"}`)); err != nil { - log.Println("lastEventHandler", err.Error()) + if len(events.Data) == 0 { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"error":"events not found"}`)) + return + } + b, _ := json.Marshal(events.Data[0]) + if _, err := w.Write(b); err != nil { + log.Println("lastEventHandler write", err) } } diff --git a/handlers/finances.go b/handlers/finances.go index 14b54c5..33afa0b 100644 --- a/handlers/finances.go +++ b/handlers/finances.go @@ -1,103 +1,30 @@ package handlers import ( - "context" "encoding/json" - "fmt" "log" "net/http" - "strconv" - "time" -) - -type Finances struct { - Balance float64 `json:"balance"` - BlockType string `json:"blockType"` - AmountSum float64 `json:"amountSum"` - TargetDate string `json:"targetDate"` - PaymentLink string `json:"paymentLink"` - Blocked bool `json:"blocked"` -} - -// Finances ... -func (h *Handler) Finances() ([]byte, error) { - var ( - body []byte - err error - client = http.DefaultClient - ) - - url := API_FINANCES - - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - request = request.WithContext(ctx) - operator := strconv.Itoa(h.Config.Operator) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json; charset=UTF-8") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, 0)) - rt.Set("Operator", operator) - rt.Set("Authorization", "Bearer "+h.Config.Token) - client.Transport = rt - - resp, err := client.Do(request) - if err != nil { - return nil, err - } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) - } - }() - - if resp.StatusCode == 409 { // Conflict (tokent already expired) - return []byte("token can't be refreshed"), nil - } - - if body, err = ReadResponseBody(resp); err != nil { - return nil, err - } - - return body, nil -} + "github.com/ad/domru/internal/api" +) -// FinancesHandler ... +// FinancesHandler now uses API wrapper. func (h *Handler) FinancesHandler(w http.ResponseWriter, r *http.Request) { - // log.Println("/financesHandler") - - data, err := h.Finances() - if err != nil { - log.Println("financesHandler", err.Error()) - } - w.Header().Set("Content-Type", "application/json") - - if _, err := w.Write(data); err != nil { - log.Println("financesHandler", err.Error()) + if h.API == nil { + writeJSONError(w, ErrAPINotInitialized) + return } -} - -func (h *Handler) GetFinances() (*Finances, error) { - finances := &Finances{} - - data, err := h.Finances() + finances, err := h.API.Finances(r.Context()) if err != nil { - return finances, err + if se := api.StatusFromError(err); se != http.StatusBadGateway { + w.WriteHeader(se) + } + writeJSONError(w, err) + return } - - if err = json.Unmarshal(data, &finances); err != nil { - return finances, fmt.Errorf("error on unmarshal Finances %q", err.Error()) + b, _ := json.Marshal(finances) + if _, err := w.Write(b); err != nil { + log.Println("financesHandler write", err) } - - return finances, nil } diff --git a/handlers/handlers.go b/handlers/handlers.go index 3cf1ba2..92957bf 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/ad/domru/config" + "github.com/ad/domru/internal/api" ) type Handler struct { @@ -16,12 +17,14 @@ type Handler struct { Account *Account TemplateFs embed.FS + API *api.Wrapper } -func NewHandlers(config *config.Config, templateFs embed.FS) (h *Handler) { +func NewHandlers(config *config.Config, templateFs embed.FS, apiWrapper *api.Wrapper) (h *Handler) { h = &Handler{ Config: config, TemplateFs: templateFs, + API: apiWrapper, } return h diff --git a/handlers/middleware.go b/handlers/middleware.go new file mode 100644 index 0000000..3782aa2 --- /dev/null +++ b/handlers/middleware.go @@ -0,0 +1,32 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/ad/domru/internal/api" +) + +// JSONErrorWriter maps generic error text patterns to HTTP status codes and writes JSON. +func JSONErrorWriter(w http.ResponseWriter, err error) { + if err == nil { + return + } + status := api.StatusFromError(err) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) +} + +// WrapJSONError converts handler returning error into one writing JSON errors. +func WrapJSONError(next func(http.ResponseWriter, *http.Request) error) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := next(w, r); err != nil { + JSONErrorWriter(w, err) + } + } +} + +// Backwards compatibility helper. +// writeJSONError kept for backward compatibility references in existing handlers. +func writeJSONError(w http.ResponseWriter, err error) { JSONErrorWriter(w, err) } diff --git a/handlers/operators.go b/handlers/operators.go index 3be10b2..0b8144d 100644 --- a/handlers/operators.go +++ b/handlers/operators.go @@ -1,71 +1,39 @@ package handlers import ( - "context" + "encoding/json" "log" "net/http" - "time" + + "github.com/ad/domru/internal/api" ) // Operators ... -func (h *Handler) Operators() (string, error) { - var ( - body []byte - err error - client = http.DefaultClient - ) - - url := API_OPERATORS - - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", err +func (h *Handler) Operators(r *http.Request) (interface{}, error) { + if h.API == nil { + return nil, api.ErrUnknown } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - request = request.WithContext(ctx) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json; charset=UTF-8") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, 0)) - client.Transport = rt - - resp, err := client.Do(request) + data, err := h.API.Operators(r.Context()) if err != nil { - return "", err + return nil, err } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) - } - }() - - // log.Printf("%#v", resp) - - if body, err = ReadResponseBody(resp); err != nil { - return "", err - } - - return string(body), nil + return data, nil } // OperatorsHandler ... func (h *Handler) OperatorsHandler(w http.ResponseWriter, r *http.Request) { - // log.Println("/operators") - - data, err := h.Operators() + w.Header().Set("Content-Type", "application/json") + data, err := h.Operators(r) if err != nil { - data = err.Error() - log.Println("operatorsHandler", err.Error()) + if se := api.StatusFromError(err); se != http.StatusBadGateway { + w.WriteHeader(se) + } + b, _ := json.Marshal(map[string]string{"error": err.Error()}) + w.Write(b) + return } - - w.Header().Set("Content-Type", "application/json") - - if _, err := w.Write([]byte(data)); err != nil { - log.Println("operatorsHandler", err.Error()) + b, _ := json.Marshal(data) + if _, err := w.Write(b); err != nil { + log.Println("operatorsHandler write", err) } } diff --git a/handlers/places.go b/handlers/places.go index d93f2e0..e69c3f3 100644 --- a/handlers/places.go +++ b/handlers/places.go @@ -1,80 +1,30 @@ package handlers import ( - "context" + "encoding/json" "log" "net/http" - "strconv" - "time" -) - -// Places ... -func (h *Handler) Places() (string, error) { - var ( - body []byte - err error - client = http.DefaultClient - ) - url := API_SUBSCRIBER_PLACES + "github.com/ad/domru/internal/api" +) - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", err +// PlacesHandler now uses API wrapper. +func (h *Handler) PlacesHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if h.API == nil { + writeJSONError(w, ErrAPINotInitialized) + return } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - request = request.WithContext(ctx) - - operator := strconv.Itoa(h.Config.Operator) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json; charset=UTF-8") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, 0)) - rt.Set("Operator", operator) - rt.Set("Authorization", "Bearer "+h.Config.Token) - client.Transport = rt - - resp, err := client.Do(request) + places, err := h.API.Places(r.Context()) if err != nil { - return "", err - } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) + if se := api.StatusFromError(err); se != http.StatusBadGateway { + w.WriteHeader(se) } - }() - - // log.Printf("Places: %s %#v", h.Config.Token, resp) - - if resp.StatusCode == 409 { // Conflict (tokent already expired) - return "token can't be refreshed", nil - } - - if body, err = ReadResponseBody(resp); err != nil { - return "", err + writeJSONError(w, err) + return } - - return string(body), nil -} - -// PlacesHandler ... -func (h *Handler) PlacesHandler(w http.ResponseWriter, r *http.Request) { - // log.Println("/placesHandler") - - data, err := h.Places() - if err != nil { - data = err.Error() - log.Println("placesHandler", err.Error()) - } - - w.Header().Set("Content-Type", "application/json") - - if _, err := w.Write([]byte(data)); err != nil { - log.Println("placesHandler", err.Error()) + b, _ := json.Marshal(places) + if _, err := w.Write(b); err != nil { + log.Println("placesHandler write", err) } } diff --git a/handlers/snapshot.go b/handlers/snapshot.go index a745a2b..6eaba8d 100644 --- a/handlers/snapshot.go +++ b/handlers/snapshot.go @@ -2,13 +2,11 @@ package handlers import ( "bytes" - "context" "fmt" jpeg "image/jpeg" "log" "net/http" "strconv" - "time" "github.com/fogleman/gg" "github.com/golang/freetype/truetype" @@ -18,64 +16,33 @@ import ( // SnapshotHandler ... func (h *Handler) SnapshotHandler(w http.ResponseWriter, r *http.Request) { var ( - body []byte - err error - client = http.DefaultClient + body []byte + err error ) query := r.URL.Query() placeID := query.Get("placeID") accessControlID := query.Get("accessControlID") - - url := fmt.Sprintf(API_VIDEO_SNAPSHOT, placeID, accessControlID) - // log.Println("/snapshotHandler", url) - - request, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Println("snapshotHandler", err) - - return - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - request = request.WithContext(ctx) - - operator := strconv.Itoa(h.Config.Operator) - - // Конвертируем placeID из строки в int64 - placeIDInt, _ := strconv.ParseInt(placeID, 10, 64) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, placeIDInt)) - rt.Set("Operator", operator) - rt.Set("Authorization", "Bearer "+h.Config.Token) - client.Transport = rt - - resp, err := client.Do(request) - if err != nil { - log.Println("snapshotHandler", "connect error") - - return - } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) - } - }() - - body, err = ReadResponseBody(resp) - - if err == nil { - contentType := http.DetectContentType(body) - - if resp.StatusCode != 200 { - err = fmt.Errorf("wrong response, code: %d, result: %s", resp.StatusCode, string(body)) - } else if contentType != "image/jpeg" { - err = fmt.Errorf("wrong response, code: %d, Content-Type: %s, result: %s", resp.StatusCode, contentType, string(body)) + if placeID == "" || accessControlID == "" { + err = fmt.Errorf("provide placeID and accessControlID") + } else { + if h.API == nil { + err = fmt.Errorf("api not initialized") + } else { + bodyBytes, upstreamErr, e := h.API.Snapshot(r.Context(), placeID, accessControlID) + if e != nil { + err = e + } else if upstreamErr != nil { + err = fmt.Errorf("snapshot upstream status %d", upstreamErr.StatusCode) + } else { + body = bodyBytes + } + if err == nil { + ct := http.DetectContentType(body) + if ct != "image/jpeg" { + err = fmt.Errorf("wrong content-type: %s", ct) + } + } } } diff --git a/handlers/stream.go b/handlers/stream.go index 1ebbff4..f92e928 100644 --- a/handlers/stream.go +++ b/handlers/stream.go @@ -1,102 +1,23 @@ package handlers import ( - "context" - "encoding/json" "fmt" - "log" "net/http" - "net/url" - "strconv" - "time" ) -// Stream ... -func (h *Handler) Stream(r *http.Request) (string, error) { - var ( - body string - respBody []byte - err error - client = http.DefaultClient - ) - - query := r.URL.Query() - cameraID := query.Get("cameraID") - - targetRawURL := fmt.Sprintf(API_CAMERA_GET_STREAM, cameraID) - - targetURL, _ := url.Parse(targetRawURL) - targetURL.RawQuery = r.URL.RawQuery - - request, err := http.NewRequest("GET", targetURL.String(), nil) - if err != nil { - return body, err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - request = request.WithContext(ctx) - - operator := strconv.Itoa(h.Config.Operator) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json; charset=UTF-8") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, 0)) - rt.Set("Operator", operator) - rt.Set("Authorization", "Bearer "+h.Config.Token) - client.Transport = rt - - resp, err := client.Do(request) - if err != nil { - return body, err - } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) - } - }() - - if resp.StatusCode == 409 { - return "token can't be refreshed", nil - } - - if respBody, err = ReadResponseBody(resp); err != nil { - return string(respBody), err - } - - type streamResponse struct { - Data struct { - URL string `json:"URL"` - } `json:"data"` - } - - var streamResp streamResponse - err = json.Unmarshal(respBody, &streamResp) - if err != nil { - return "", fmt.Errorf("json parse error: %w", err) - } - - return streamResp.Data.URL, nil -} - -// StreamHandler ... +// StreamHandler now builds redirect via API wrapper. func (h *Handler) StreamHandler(w http.ResponseWriter, r *http.Request) { - // log.Println("/streamHandler") - - data, err := h.Stream(r) - if err != nil { - data = err.Error() - log.Println("streamHandler", err.Error()) - - if _, err := w.Write([]byte(data)); err != nil { - log.Println("streamHandler", err.Error()) - } - + if h.API == nil { + w.Header().Set("Content-Type", "application/json") + writeJSONError(w, ErrAPINotInitialized) return } - - http.Redirect(w, r, data, http.StatusFound) + cameraID := r.URL.Query().Get("cameraID") + if cameraID == "" { + w.Header().Set("Content-Type", "application/json") + writeJSONError(w, fmt.Errorf("provide cameraID")) + return + } + target := h.API.StreamURL(cameraID, r.URL.Query()) + http.Redirect(w, r, target, http.StatusFound) } diff --git a/handlers/token.go b/handlers/token.go deleted file mode 100644 index 695b0ef..0000000 --- a/handlers/token.go +++ /dev/null @@ -1,68 +0,0 @@ -package handlers - -import ( - "context" - "encoding/json" - "log" - "net/http" - "strconv" - "time" -) - -// Refresh ... -func (h *Handler) Refresh(refreshToken *string) (string, string, error) { - var ( - body []byte - err error - client = http.DefaultClient - ) - - url := API_REFRESH_SESSION - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", "", err - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - request = request.WithContext(ctx) - - operator := strconv.Itoa(h.Config.Operator) - - rt := WithHeader(client.Transport) - rt.Set("Host", API_HOST) - rt.Set("Content-Type", "application/json; charset=UTF-8") - rt.Set("User-Agent", GenerateUserAgent(h.Config.Operator, h.Config.UUID, 0)) - rt.Set("Operator", operator) - rt.Set("Bearer", h.Config.RefreshToken) - client.Transport = rt - - resp, err := client.Do(request) - if err != nil { - return "", "", err - } - - defer func() { - err2 := resp.Body.Close() - if err2 != nil { - log.Println(err2) - } - }() - - // log.Printf("%#v", resp) - - if resp.StatusCode == 409 { // Conflict (tokent already expired) - return "token can't be refreshed", "", nil - } - - if body, err = ReadResponseBody(resp); err != nil { - return "", "", err - } - - var authResp ConfirmResponse - if err = json.Unmarshal(body, &authResp); err != nil { - return "", "", err - } - - return authResp.AccessToken, authResp.RefreshToken, nil -} diff --git a/handlers/types.go b/handlers/types.go new file mode 100644 index 0000000..14b206c --- /dev/null +++ b/handlers/types.go @@ -0,0 +1,45 @@ +package handlers + +import "github.com/ad/domru/internal/models" + +// Page data structs used by templates +type AccountsPageData struct { + Accounts []Account + Phone string + HassioIngress string + LoginError string +} + +type LoginPageData struct { + LoginError string + Phone string + HassioIngress string +} + +type SMSPageData struct { + Phone string + Index string + HassioIngress string + LoginError string +} + +// Aliases to centralized domain models (removed duplicate struct definitions) +type Account = models.Account +type ConfirmRequest = models.ConfirmRequest +type ConfirmResponse = models.ConfirmResponse +type Cameras = models.Cameras +type Places = models.Places +type Finances = models.Finances +type HomePageData = models.HomePageData + +// HAConfig mirrors Home Assistant network info response +type HAConfig struct { + Result string `json:"result"` + Data struct { + Interfaces []struct { + Ipv4 struct { + Address []string `json:"address"` + } `json:"ipv4"` + } `json:"interfaces"` + } `json:"data"` +} diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..f195cdd --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,168 @@ +package api + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + + "github.com/ad/domru/internal/constants" + "github.com/ad/domru/internal/models" + "github.com/ad/domru/internal/upstream" +) + +var baseURL = "https://" + constants.APIHost + +type HTTPDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Wrapper groups high-level API calls +type DeviceInfo interface { + GetOperatorID() int + GetUUID() string +} + +type Wrapper struct { + client HTTPDoer + logger Logger + device DeviceInfo +} + +func New(client HTTPDoer) *Wrapper { return &Wrapper{client: client} } +func (w *Wrapper) WithLogger(l Logger) *Wrapper { w.logger = l; return w } +func (w *Wrapper) WithDevice(d DeviceInfo) *Wrapper { w.device = d; return w } + +// Logger lightweight interface to avoid dependency on concrete logging lib. +type Logger interface { + Info(msg string, kv ...any) + Error(msg string, kv ...any) +} + +func (w *Wrapper) fetchJSON(ctx context.Context, url string, out interface{}, placeID int64) error { + req := upstream.New(url).WithMethod(http.MethodGet).WithContext(ctx) + if uc, ok := w.client.(*http.Client); ok { + req.WithClient(uc) + } + if w.logger != nil { + req.WithLogger(w.logger) + } + // dynamic headers + if w.device != nil { + ua := buildUserAgent(w.device.GetOperatorID(), w.device.GetUUID(), placeID) + req.Set("User-Agent", ua) + } + req.Set("Host", constants.APIHost) + if err := req.Send(out); err != nil { + return mapUpstreamErr(err) + } + return nil +} + +func buildUserAgent(operatorID int, uuid string, placeID int64) string { + if placeID == 0 { + placeID = 1 + } + return fmt.Sprintf("%s | | %d | %s | %d", constants.BaseUserAgentCore, operatorID, uuid, placeID) +} + +func (w *Wrapper) Cameras(ctx context.Context) (models.Cameras, error) { + var out models.Cameras + err := w.fetchJSON(ctx, fmt.Sprintf("%s/rest/v1/forpost/cameras", baseURL), &out, 0) + return out, err +} + +func (w *Wrapper) Places(ctx context.Context) (models.Places, error) { + var out models.Places + err := w.fetchJSON(ctx, fmt.Sprintf("%s/rest/v1/subscriberplaces", baseURL), &out, 0) + return out, err +} + +func (w *Wrapper) Finances(ctx context.Context) (models.Finances, error) { + var out models.Finances + err := w.fetchJSON(ctx, fmt.Sprintf("%s/rest/v1/subscribers/profiles/finances", baseURL), &out, 0) + return out, err +} + +func (w *Wrapper) Events(ctx context.Context, placeID string) (models.EventsInputModel, error) { + var out models.EventsInputModel + err := w.fetchJSON(ctx, fmt.Sprintf("%s/rest/v1/places/%s/events?allowExtentedActions=true", baseURL, placeID), &out, 0) + return out, err +} + +func (w *Wrapper) StreamURL(cameraID string, q url.Values) string { + u := fmt.Sprintf("%s/rest/v1/forpost/cameras/%s/video", baseURL, cameraID) + if len(q) > 0 { + return u + "?" + q.Encode() + } + return u +} + +func (w *Wrapper) SnapshotURL(placeID, accessControlID string) string { + return fmt.Sprintf("%s/rest/v1/places/%s/accesscontrols/%s/videosnapshots", baseURL, placeID, accessControlID) +} + +func (w *Wrapper) OpenDoorURL(placeID, accessControlID string) string { + return fmt.Sprintf("%s/rest/v1/places/%s/accesscontrols/%s/actions", baseURL, placeID, accessControlID) +} + +// Operators returns raw operators JSON (simple pass-through, rarely used) +func (w *Wrapper) Operators(ctx context.Context) (map[string]any, error) { + url := fmt.Sprintf("%s/public/v1/operators", baseURL) + var out map[string]any + if err := w.fetchJSON(ctx, url, &out, 0); err != nil { + return nil, err + } + return out, nil +} + +// OpenDoor triggers an access control action +func (w *Wrapper) OpenDoor(ctx context.Context, placeID, accessControlID string) error { + url := w.OpenDoorURL(placeID, accessControlID) + payload := map[string]string{"name": "accessControlOpen"} + req := upstream.New(url).WithMethod(http.MethodPost).WithJSONBody(payload).WithContext(ctx) + if uc, ok := w.client.(*http.Client); ok { + req.WithClient(uc) + } + if w.logger != nil { + req.WithLogger(w.logger) + } + pid, _ := strconv.ParseInt(placeID, 10, 64) + if w.device != nil { + req.Set("User-Agent", buildUserAgent(w.device.GetOperatorID(), w.device.GetUUID(), pid)) + } + req.Set("Host", constants.APIHost) + if err := req.Send(nil); err != nil { + return mapUpstreamErr(err) + } + return nil +} + +// Snapshot fetches snapshot bytes and returns them (content-type assumed image/jpeg upstream) +func (w *Wrapper) Snapshot(ctx context.Context, placeID, accessControlID string) ([]byte, *upstream.UpstreamError, error) { + url := w.SnapshotURL(placeID, accessControlID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, nil, err + } + pid, _ := strconv.ParseInt(placeID, 10, 64) + if w.device != nil { + req.Header.Set("User-Agent", buildUserAgent(w.device.GetOperatorID(), w.device.GetUUID(), pid)) + } + req.Host = constants.APIHost + resp, err := w.client.Do(req) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, err + } + if resp.StatusCode >= 400 { + return nil, &upstream.UpstreamError{StatusCode: resp.StatusCode, Body: string(data)}, nil + } + return data, nil, nil +} diff --git a/internal/api/auth.go b/internal/api/auth.go new file mode 100644 index 0000000..a5be006 --- /dev/null +++ b/internal/api/auth.go @@ -0,0 +1,68 @@ +package api + +import ( + "context" + "fmt" + "net/http" + + "github.com/ad/domru/internal/constants" + "github.com/ad/domru/internal/models" + "github.com/ad/domru/internal/upstream" +) + +// Auth related endpoints encapsulation (Step 1 of refactor) + +const ( + loginURLFmt = "https://%s/auth/v2/login/%s" + confirmURLFmt = "https://%s/auth/v2/confirmation/%s" + confirmSMSURLFmt = "https://%s/auth/v2/auth/%s/confirmation" + refreshSessionURL = "https://%s/auth/v2/session/refresh" +) + +// Accounts retrieves accounts for login (phone) +func (w *Wrapper) Accounts(ctx context.Context, login string) ([]models.Account, error) { + var out []models.Account + url := fmt.Sprintf(loginURLFmt, constants.APIHost, login) + r := upstream.New(url).WithMethod(http.MethodGet).WithContext(ctx) + if err := r.Send(&out); err != nil { + return nil, mapUpstreamErr(err) + } + return out, nil +} + +// RequestCode triggers SMS code sending for selected account +func (w *Wrapper) RequestCode(ctx context.Context, login string, account models.Account) error { + url := fmt.Sprintf(confirmURLFmt, constants.APIHost, login) + r := upstream.New(url).WithMethod(http.MethodPost).WithJSONBody(account).WithContext(ctx) + if err := r.Send(nil); err != nil { + return mapUpstreamErr(err) + } + return nil +} + +// ConfirmCode validates SMS code and returns tokens +func (w *Wrapper) ConfirmCode(ctx context.Context, login, code string, account models.Account) (string, string, error) { + url := fmt.Sprintf(confirmSMSURLFmt, constants.APIHost, login) + payload := models.ConfirmRequest{Confirm: code, SubscriberID: account.SubscriberID, Login: login, OperatorID: account.OperatorID, AccountID: account.AccountID, ProfileID: account.ProfileID} + var resp models.ConfirmResponse + r := upstream.New(url).WithMethod(http.MethodPost).WithJSONBody(payload).WithContext(ctx) + if err := r.Send(&resp); err != nil { + return "", "", mapUpstreamErr(err) + } + return resp.AccessToken, resp.RefreshToken, nil +} + +// Refresh tokens using refresh token stored in config provider (handled outside normally). +func (w *Wrapper) Refresh(ctx context.Context, refreshToken string, operatorID int, ua string) (string, string, error) { + r := upstream.New(fmt.Sprintf(refreshSessionURL, constants.APIHost)).WithMethod(http.MethodGet).WithContext(ctx) + // Custom headers + r.Set("Bearer", refreshToken) + if ua != "" { + r.Set("User-Agent", ua) + } + var out models.ConfirmResponse + if err := r.Send(&out); err != nil { + return "", "", mapUpstreamErr(err) + } + return out.AccessToken, out.RefreshToken, nil +} diff --git a/internal/api/errors.go b/internal/api/errors.go new file mode 100644 index 0000000..9ef84da --- /dev/null +++ b/internal/api/errors.go @@ -0,0 +1,55 @@ +package api + +import ( + "errors" + "net/http" + + "github.com/ad/domru/internal/upstream" +) + +// Sentinel errors (Step 5) +var ( + ErrUnauthorized = errors.New("unauthorized") + ErrForbidden = errors.New("forbidden") + ErrNotFound = errors.New("not found") + ErrBadRequest = errors.New("bad request") + ErrTokenExpired = errors.New("token expired") + ErrUnknown = errors.New("unknown upstream error") +) + +// mapUpstreamErr converts upstream errors to sentinel ones +func mapUpstreamErr(err error) error { + if err == nil { + return nil + } + if ue, ok := err.(*upstream.UpstreamError); ok { + switch ue.StatusCode { + case http.StatusBadRequest: + return ErrBadRequest + case http.StatusUnauthorized: + return ErrUnauthorized + case http.StatusForbidden: + return ErrForbidden + case http.StatusNotFound: + return ErrNotFound + case 409: + return ErrTokenExpired + default: + return ErrUnknown + } + } + return err +} + +// StatusFromError maps sentinel error to HTTP code +func StatusFromError(err error) int { + switch err { + case ErrBadRequest: + return http.StatusBadRequest + case ErrUnauthorized, ErrForbidden, ErrTokenExpired: + return http.StatusUnauthorized + case ErrNotFound: + return http.StatusNotFound + } + return http.StatusBadGateway +} diff --git a/internal/auth/client.go b/internal/auth/client.go new file mode 100644 index 0000000..d8f82ab --- /dev/null +++ b/internal/auth/client.go @@ -0,0 +1,76 @@ +package auth + +import ( + "errors" + "net/http" + "strconv" +) + +// TokenProvider supplies current access token +type TokenProvider interface{ GetToken() (string, error) } + +// TokenRefresher refreshes token when expired +type TokenRefresher interface{ RefreshToken() error } + +// OperatorProvider supplies operator id +type OperatorProvider interface{ GetOperatorID() (int, error) } + +// AutoClient injects Authorization / Operator headers and retries once after refresh. +type AutoClient struct { + Base *http.Client + Tokens TokenProvider + Refresher TokenRefresher + Operators OperatorProvider + UserAgent string +} + +func NewAutoClient(base *http.Client, tp TokenProvider, tr TokenRefresher, op OperatorProvider, ua string) *AutoClient { + if base == nil { + base = http.DefaultClient + } + return &AutoClient{Base: base, Tokens: tp, Refresher: tr, Operators: op, UserAgent: ua} +} + +func (c *AutoClient) Do(req *http.Request) (*http.Response, error) { + if err := c.decorate(req); err != nil { + return nil, err + } + resp, err := c.Base.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden || resp.StatusCode == 409 { + // try refresh once + resp.Body.Close() + if c.Refresher != nil { + if rErr := c.Refresher.RefreshToken(); rErr == nil { + if err := c.decorate(req); err != nil { + return nil, err + } + return c.Base.Do(req) + } + } + } + return resp, nil +} + +func (c *AutoClient) decorate(req *http.Request) error { + token, err := c.Tokens.GetToken() + if err != nil { + return err + } + op, err := c.Operators.GetOperatorID() + if err != nil { + return err + } + if token == "" { + return errors.New("empty token") + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Operator", strconv.Itoa(op)) + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + return nil +} diff --git a/internal/auth/providers.go b/internal/auth/providers.go new file mode 100644 index 0000000..db4e1b3 --- /dev/null +++ b/internal/auth/providers.go @@ -0,0 +1,11 @@ +package auth + +import "github.com/ad/domru/config" + +// Config-backed providers + +type ConfigProvider struct{ Cfg *config.Config } + +func (p *ConfigProvider) GetToken() (string, error) { return p.Cfg.Token, nil } +func (p *ConfigProvider) RefreshToken() error { return nil } // will be wired later with real refresh logic +func (p *ConfigProvider) GetOperatorID() (int, error) { return p.Cfg.Operator, nil } diff --git a/internal/auth/refresh.go b/internal/auth/refresh.go new file mode 100644 index 0000000..59fb49b --- /dev/null +++ b/internal/auth/refresh.go @@ -0,0 +1,63 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/ad/domru/internal/constants" +) + +type RefreshConfig interface { + GetRefreshToken() string + SetTokens(access, refresh string) error + GetOperatorID() int + GetUUID() string +} + +type RefreshResponse struct { + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` +} + +type HTTPDoer interface { + Do(*http.Request) (*http.Response, error) +} + +type TokenRefresherImpl struct { + Cfg RefreshConfig + Client HTTPDoer +} + +const refreshURL = "https://%s/auth/v2/session/refresh" + +func (r *TokenRefresherImpl) RefreshToken() error { + rtok := r.Cfg.GetRefreshToken() + if rtok == "" { + return fmt.Errorf("no refresh token") + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(refreshURL, constants.APIHost), nil) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + req.Header.Set("Bearer", rtok) + req.Header.Set("User-Agent", constants.BaseUserAgentCore) + resp, err := r.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode >= 400 { + return fmt.Errorf("refresh status %d", resp.StatusCode) + } + var rr RefreshResponse + if err := json.NewDecoder(resp.Body).Decode(&rr); err != nil { + return err + } + return r.Cfg.SetTokens(rr.AccessToken, rr.RefreshToken) +} diff --git a/internal/constants/constants.go b/internal/constants/constants.go new file mode 100644 index 0000000..471073e --- /dev/null +++ b/internal/constants/constants.go @@ -0,0 +1,7 @@ +package constants + +const ( + APIHost = "myhome.proptech.ru" + BaseUserAgentCore = "Google sdkgphone64x8664 | Android 14 | erth | 8.26.0 (82600010)" + HANetworkInfoURL = "http://supervisor/network/info" +) diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..d3588ea --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,111 @@ +package models + +// Centralized data structures migrated from handlers. + +type Account struct { + OperatorID int64 `json:"operatorId"` + SubscriberID int64 `json:"subscriberId"` + AccountID string `json:"accountId"` + PlaceID int64 `json:"placeId"` + Address string `json:"address"` + ProfileID string `json:"profileId"` +} + +type ConfirmRequest struct { + Confirm string `json:"confirm1"` + SubscriberID int64 `json:"subscriberId"` + Login string `json:"login"` + OperatorID int64 `json:"operatorId"` + AccountID string `json:"accountId"` + ProfileID string `json:"profileId"` +} + +type ConfirmResponse struct { + OperatorID int64 `json:"operatorId"` + TokenType string `json:"tokenType"` + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` +} + +type Places struct { + Data []struct { + ID int `json:"id"` + Place struct { + ID int `json:"id"` + Address struct { + VisibleAddress string `json:"visibleAddress"` + } `json:"address"` + AccessControls []struct { + ID int `json:"id"` + Name string `json:"name"` + } `json:"accessControls"` + } `json:"place"` + Subscriber struct { + ID int `json:"id"` + Name string `json:"name"` + AccountID string `json:"accountId"` + } `json:"subscriber"` + Blocked bool `json:"blocked"` + } `json:"data"` +} + +type Cameras struct { + Data []struct { + ID int `json:"ID"` + Name string `json:"Name"` + IsActive int `json:"IsActive"` + } `json:"data"` +} + +type Finances struct { + Balance float64 `json:"balance"` + BlockType string `json:"blockType"` + AmountSum float64 `json:"amountSum"` + TargetDate string `json:"targetDate"` + PaymentLink string `json:"paymentLink"` + Blocked bool `json:"blocked"` +} + +type EventsInputModel struct { + Data []struct { + ID string `json:"id,omitempty"` + PlaceID int `json:"placeId,omitempty"` + EventTypeName string `json:"eventTypeName,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Message string `json:"message,omitempty"` + Source struct { + Type string `json:"type,omitempty"` + ID int `json:"id,omitempty"` + } `json:"source,omitempty"` + Value struct { + Type string `json:"type,omitempty"` + Value bool `json:"value,omitempty"` + } `json:"value,omitempty"` + EventStatusValue interface{} `json:"eventStatusValue,omitempty"` + Actions []interface{} `json:"actions,omitempty"` + } `json:"data,omitempty"` +} + +type HomePageData struct { + HassioIngress string + HostIP string + Port string + LoginError string + Phone string + Token string + RefreshToken string + Cameras Cameras + Places Places + Finances Finances +} + +type HAConfig struct { + Result string `json:"result"` + Data struct { + Interfaces []struct { + Ipv4 struct { + Address []string `json:"address"` + } `json:"ipv4"` + } `json:"interfaces"` + } `json:"data"` +} diff --git a/internal/upstream/request.go b/internal/upstream/request.go new file mode 100644 index 0000000..266c360 --- /dev/null +++ b/internal/upstream/request.go @@ -0,0 +1,119 @@ +package upstream + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" + + "github.com/ad/domru/internal/constants" +) + +// Default headers similar to mobile app +var defaultHeaders = map[string]string{ + "user-agent": constants.BaseUserAgentCore, + "content-type": "application/json; charset=UTF-8", + "connection": "Keep-Alive", + "accept-encoding": "gzip", +} + +// Error from upstream service +type UpstreamError struct { + StatusCode int + Body string +} + +func (e *UpstreamError) Error() string { + return fmt.Sprintf("upstream error: %d, body: %s", e.StatusCode, e.Body) +} + +// Request builder +type Request struct { + client *http.Client + url string + method string + body []byte + headers http.Header + start time.Time + ctx context.Context + logger Logger +} + +func New(url string) *Request { + h := http.Header{} + for k, v := range defaultHeaders { + h.Set(k, v) + } + return &Request{client: http.DefaultClient, url: url, headers: h, ctx: context.Background()} +} + +func (r *Request) WithClient(c *http.Client) *Request { r.client = c; return r } +func (r *Request) WithMethod(m string) *Request { r.method = m; return r } +func (r *Request) WithJSONBody(v interface{}) *Request { + b, _ := json.Marshal(v) + r.body = b + return r +} +func (r *Request) Set(k, v string) *Request { r.headers.Set(k, v); return r } +func (r *Request) WithContext(ctx context.Context) *Request { + if ctx != nil { + r.ctx = ctx + } + return r +} +func (r *Request) WithLogger(l Logger) *Request { r.logger = l; return r } + +// Logger interface (duplicated lightweight to avoid circular import). Implemented in higher layers. +type Logger interface { + Info(msg string, kv ...any) + Error(msg string, kv ...any) +} + +func (r *Request) Send(out interface{}) error { + if r.method == "" { + r.method = http.MethodGet + } + var bodyReader io.Reader + if len(r.body) > 0 { + bodyReader = bytes.NewReader(r.body) + } + req, err := http.NewRequestWithContext(r.ctx, r.method, r.url, bodyReader) + if err != nil { + return err + } + for k, vals := range r.headers { + for _, v := range vals { + req.Header.Add(k, v) + } + } + start := time.Now() + resp, err := r.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + // gzip handled elsewhere if needed via transport; just read + data, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read body: %w", err) + } + if r.logger != nil { + r.logger.Info("upstream request", "method", r.method, "url", r.url, "dur", time.Since(start).String(), "status", resp.StatusCode) + } else { + log.Printf("upstream %s %s took %s status=%d", r.method, r.url, time.Since(start), resp.StatusCode) + } + if resp.StatusCode >= 400 { + return &UpstreamError{StatusCode: resp.StatusCode, Body: string(data)} + } + if out == nil || len(data) == 0 { + return nil + } + if err := json.Unmarshal(data, out); err != nil { + return fmt.Errorf("json decode: %w", err) + } + return nil +} diff --git a/main.go b/main.go index 8bf3a0d..42c6aa6 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "embed" "log" "net/http" @@ -8,8 +9,17 @@ import ( "github.com/ad/domru/config" "github.com/ad/domru/handlers" + "github.com/ad/domru/internal/api" + myauth "github.com/ad/domru/internal/auth" + "github.com/ad/domru/internal/constants" ) +// basicLogger implements api.Logger +type basicLogger struct{} + +func (l basicLogger) Info(msg string, kv ...any) { log.Println(append([]any{"INFO", msg}, kv...)...) } +func (l basicLogger) Error(msg string, kv ...any) { log.Println(append([]any{"ERROR", msg}, kv...)...) } + //go:embed templates/* var templateFs embed.FS @@ -17,22 +27,25 @@ func main() { // Init Config addonConfig := config.InitConfig() - // Init Handlers - h := handlers.NewHandlers(addonConfig, templateFs) + // Init auth + api + tp := &myauth.ConfigProvider{Cfg: addonConfig} + refresher := &myauth.TokenRefresherImpl{Cfg: addonConfig, Client: http.DefaultClient} + autoClient := myauth.NewAutoClient(http.DefaultClient, tp, refresher, tp, constants.BaseUserAgentCore) + apiWrapper := api.New(autoClient).WithLogger(basicLogger{}).WithDevice(addonConfig) + + // Init Handlers with API wrapper + h := handlers.NewHandlers(addonConfig, templateFs, apiWrapper) switch { case addonConfig.Token != "" || addonConfig.RefreshToken != "": if addonConfig.RefreshToken != "" { - access, refresh, err := h.Refresh(&addonConfig.RefreshToken) + access, refresh, err := apiWrapper.Refresh(context.Background(), addonConfig.RefreshToken, addonConfig.Operator, addonConfig.UUID) if err != nil { - log.Println("refresh token, error:", err.Error()) + log.Println("refresh token error:", err) } else { addonConfig.Token = access addonConfig.RefreshToken = refresh - - if err = addonConfig.WriteConfig(); err != nil { - log.Println("error on write config file ", err) - } + _ = addonConfig.WriteConfig() } } default: