Skip to content

Commit

Permalink
Implement new tesla API (#264)
Browse files Browse the repository at this point in the history
* Implement new tesla api, create api route for retrieving vehicles using new api, handle tesla auth, write tests to validate new endpoint

* WIP: Implement PR review change requesy

* Add Tesla secrets, rename a little bit

* Implement suggested changes from PR review

* Move struct up

* Rename setting again

* Fix lint errors

* Try to fix up Fleet API URL setting usage

---------

Co-authored-by: Dylan Moreland <[email protected]>
  • Loading branch information
0xdev22 and elffjs authored Feb 21, 2024
1 parent 9660e98 commit 5dc07e2
Show file tree
Hide file tree
Showing 13 changed files with 618 additions and 4 deletions.
8 changes: 8 additions & 0 deletions charts/devices-api/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ spec:
- remoteRef:
key: {{ .Release.Namespace }}/vc/vin/issuer_private_key
secretKey: ISSUER_PRIVATE_KEY
{{- if eq .Release.Namespace "dev" }}
- remoteRef:
key: {{ .Release.Namespace }}/devices/tesla/client_id
secretKey: TESLA_CLIENT_ID
- remoteRef:
key: {{ .Release.Namespace }}/devices/tesla/client_secret
secretKey: TESLA_CLIENT_SECRET
{{- end }}
secretStoreRef:
kind: ClusterSecretStore
name: aws-secretsmanager-secret-store
Expand Down
2 changes: 2 additions & 0 deletions charts/devices-api/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ env:
DEVICE_DATA_GRPC_ADDR: device-data-api-dev:8086
SYNTHETIC_FINGERPRINT_TOPIC: topic.synthetic.fingerprint
SYNTHETIC_FINGERPRINT_CONSUMER_GROUP: consumer.synthetic.fingerprint
TESLA_TOKEN_URL: https://auth.tesla.com/oauth2/v3/token
TESLA_FLEET_API: https://fleet-api.prd.%s.vn.cloud.tesla.com
service:
type: ClusterIP
ports:
Expand Down
5 changes: 4 additions & 1 deletion cmd/devices-api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/base64"
"github.com/DIMO-Network/devices-api/internal/services/fingerprint"
"math/big"
"net"
"os"
Expand Down Expand Up @@ -35,7 +36,6 @@ import (
"github.com/DIMO-Network/devices-api/internal/controllers"
"github.com/DIMO-Network/devices-api/internal/services"
"github.com/DIMO-Network/devices-api/internal/services/autopi"
"github.com/DIMO-Network/devices-api/internal/services/fingerprint"
"github.com/DIMO-Network/devices-api/internal/services/issuer"
"github.com/DIMO-Network/devices-api/internal/services/macaron"
"github.com/DIMO-Network/devices-api/internal/services/registry"
Expand Down Expand Up @@ -102,6 +102,7 @@ func startWebAPI(logger zerolog.Logger, settings *config.Settings, pdb db.Store,
smartcarClient := services.NewSmartcarClient(settings)
teslaTaskService := services.NewTeslaTaskService(settings, producer)
teslaSvc := services.NewTeslaService(settings)
teslaFleetAPISvc := services.NewTeslaFleetAPIService(settings, &logger)
autoPiSvc := services.NewAutoPiAPIService(settings, pdb.DBS)
valuationsSvc := services.NewValuationsAPIService(settings, &logger)
autoPiIngest := services.NewIngestRegistrar(producer)
Expand Down Expand Up @@ -144,6 +145,7 @@ func startWebAPI(logger zerolog.Logger, settings *config.Settings, pdb db.Store,
webhooksController := controllers.NewWebhooksController(settings, pdb.DBS, &logger, autoPiSvc, ddIntSvc)
documentsController := controllers.NewDocumentsController(settings, &logger, s3ServiceClient, pdb.DBS)
countriesController := controllers.NewCountriesController()
userIntegrationAuthController := controllers.NewUserIntegrationAuthController(settings, pdb.DBS, &logger, ddSvc, teslaFleetAPISvc)

// commenting this out b/c the library includes the path in the metrics which saturates prometheus queries - need to fork / make our own
//prometheus := fiberprometheus.New("devices-api")
Expand Down Expand Up @@ -232,6 +234,7 @@ func startWebAPI(logger zerolog.Logger, settings *config.Settings, pdb db.Store,
v1Auth.Post("/user/devices/fromvin", userDeviceController.RegisterDeviceForUserFromVIN)
v1Auth.Post("/user/devices/fromsmartcar", userDeviceController.RegisterDeviceForUserFromSmartcar)
v1Auth.Post("/user/devices", userDeviceController.RegisterDeviceForUser)
v1Auth.Post("/integration/:tokenID/credentials", userIntegrationAuthController.CompleteOAuthExchange)

v1Auth.Get("/integrations", userDeviceController.GetIntegrations)

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ require (
go.uber.org/automaxprocs v1.5.1
go.uber.org/mock v0.3.0
golang.org/x/mod v0.14.0
golang.org/x/oauth2 v0.13.0
)

require (
Expand Down Expand Up @@ -104,6 +105,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
)
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,8 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -1340,6 +1342,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
Expand Down Expand Up @@ -1471,6 +1474,8 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
Expand Down
8 changes: 8 additions & 0 deletions internal/config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,16 @@ type Settings struct {

SyntheticFingerprintTopic string `yaml:"SYNTHETIC_FINGERPRINT_TOPIC"`
SyntheticFingerprintConsumerGroup string `yaml:"SYNTHETIC_FINGERPRINT_CONSUMER_GROUP"`
Tesla Tesla `yaml:"TESLA"`
}

func (s *Settings) IsProduction() bool {
return s.Environment == "prod" // this string is set in the helm chart values-prod.yaml
}

type Tesla struct {
ClientID string `yaml:"CLIENT_ID"`
ClientSecret string `yaml:"CLIENT_SECRET"`
TokenURL string `yaml:"TOKEN_URL"`
FleetAPI string `yaml:"FLEET_API"`
}
2 changes: 1 addition & 1 deletion internal/controllers/user_devices_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ func (s *UserDevicesControllerTestSuite) SetupSuite() {
app.Get("/user/devices/:userDeviceID/valuations", test.AuthInjectorTestHandler(s.testUserID), c.GetValuations)
app.Get("/user/devices/:userDeviceID/range", test.AuthInjectorTestHandler(s.testUserID), c.GetRange)
app.Post("/user/devices/:userDeviceID/commands/refresh", test.AuthInjectorTestHandler(s.testUserID), c.RefreshUserDeviceStatus)
s.controller = &c

s.controller = &c
s.app = app
}

Expand Down
149 changes: 149 additions & 0 deletions internal/controllers/user_integrations_auth_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package controllers

import (
"github.com/DIMO-Network/devices-api/internal/config"
"github.com/DIMO-Network/devices-api/internal/constants"
"github.com/DIMO-Network/devices-api/internal/services"
"github.com/DIMO-Network/shared/db"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
"strconv"
)

type UserIntegrationAuthController struct {
Settings *config.Settings
DBS func() *db.ReaderWriter
DeviceDefSvc services.DeviceDefinitionService
log *zerolog.Logger
teslaFleetAPISvc services.TeslaFleetAPIService
}

func NewUserIntegrationAuthController(
settings *config.Settings,
dbs func() *db.ReaderWriter,
logger *zerolog.Logger,
ddSvc services.DeviceDefinitionService,
teslaFleetAPISvc services.TeslaFleetAPIService,
) UserIntegrationAuthController {
return UserIntegrationAuthController{
Settings: settings,
DBS: dbs,
DeviceDefSvc: ddSvc,
log: logger,
teslaFleetAPISvc: teslaFleetAPISvc,
}
}

// CompleteOAuthExchangeResponseWrapper response wrapper for list of user vehicles
type CompleteOAuthExchangeResponseWrapper struct {
Vehicles []CompleteOAuthExchangeResponse `json:"vehicles"`
}

// CompleteOAuthExchangeRequest request object for completing tesla OAuth
type CompleteOAuthExchangeRequest struct {
AuthorizationCode string `json:"authorizationCode"`
RedirectURI string `json:"redirectUri"`
Region string `json:"region"`
}

// CompleteOAuthExchangeResponse response object for tesla vehicles attached to user account
type CompleteOAuthExchangeResponse struct {
ExternalID string `json:"externalId"`
VIN string `json:"vin"`
Definition DeviceDefinition `json:"definition"`
}

// DeviceDefinition inner definition object containing meta data for each tesla vehicle
type DeviceDefinition struct {
Make string `json:"make"`
Model string `json:"model"`
Year int `json:"year"`
DeviceDefinitionID string `json:"id"`
}

// CompleteOAuthExchange godoc
// @Description Complete Tesla auth and get devices for authenticated user
// @Tags user-devices
// @Produce json
// @Accept json
// @Param tokenID path string true "token id for integration"
// @Param user_device body controllers.CompleteOAuthExchangeRequest true "all fields are required"
// @Security ApiKeyAuth
// @Success 200 {object} controllers.CompleteOAuthExchangeResponseWrapper
// @Security BearerAuth
// @Router /integration/:tokenID/credentials [post]
func (u *UserIntegrationAuthController) CompleteOAuthExchange(c *fiber.Ctx) error {
tokenID := c.Params("tokenID")

tkID, err := strconv.ParseUint(tokenID, 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "could not process the provided tokenId!")
}

intd, err := u.DeviceDefSvc.GetIntegrationByTokenID(c.Context(), tkID)
if err != nil {
u.log.Err(err).Str("Calling Function", "GetIntegrationByTokenID").Uint64("tokenID", tkID).Msg("Error occurred trying to get integration using tokenID")
return fiber.NewError(fiber.StatusInternalServerError, "an error occurred completing authorization")
}
if intd.Vendor != constants.TeslaVendor {
return fiber.NewError(fiber.StatusBadRequest, "invalid value provided for tokenId!")
}

reqBody := new(CompleteOAuthExchangeRequest)
if err := c.BodyParser(reqBody); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Couldn't parse request JSON body.")
}

if reqBody.Region != "na" && reqBody.Region != "eu" {
return fiber.NewError(fiber.StatusBadRequest, "invalid value provided for region, only na and eu are allowed")
}

logger := u.log.With().
Str("region", reqBody.Region).
Str("redirectUri", reqBody.RedirectURI).
Str("route", c.Route().Path).
Logger()
logger.Info().Msg("Attempting to complete Tesla authorization")

teslaAuth, err := u.teslaFleetAPISvc.CompleteTeslaAuthCodeExchange(c.Context(), reqBody.AuthorizationCode, reqBody.RedirectURI, reqBody.Region)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to get tesla authCode:"+err.Error())
}

vehicles, err := u.teslaFleetAPISvc.GetVehicles(c.Context(), teslaAuth.AccessToken, reqBody.Region)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "error occurred fetching vehicles:"+err.Error())
}

response := make([]CompleteOAuthExchangeResponse, 0, len(vehicles))
for _, v := range vehicles {
decodeVIN, err := u.DeviceDefSvc.DecodeVIN(c.Context(), v.VIN, "", 0, "")
if err != nil || len(decodeVIN.DeviceDefinitionId) == 0 {
u.log.Err(err).Msg("An error occurred decoding vin for tesla vehicle")
return fiber.NewError(fiber.StatusFailedDependency, "An error occurred completing tesla authorization")
}

dd, err := u.DeviceDefSvc.GetDeviceDefinitionByID(c.Context(), decodeVIN.DeviceDefinitionId)
if err != nil || len(decodeVIN.DeviceDefinitionId) == 0 {
u.log.Err(err).Str("deviceDefinitionID", decodeVIN.DeviceDefinitionId).Msg("An error occurred fetching device definition using ID")
return fiber.NewError(fiber.StatusFailedDependency, "An error occurred completing tesla authorization")
}

response = append(response, CompleteOAuthExchangeResponse{
ExternalID: strconv.Itoa(v.ID),
VIN: v.VIN,
Definition: DeviceDefinition{
Make: dd.Type.Make,
Model: dd.Type.Model,
Year: int(dd.Type.Year),
DeviceDefinitionID: decodeVIN.DeviceDefinitionId,
},
})
}

vehicleResp := &CompleteOAuthExchangeResponseWrapper{
Vehicles: response,
}

return c.JSON(vehicleResp)
}
Loading

0 comments on commit 5dc07e2

Please sign in to comment.