Add depot, attendant, jetfire, optimus, ota services with kustomize overlays

This commit is contained in:
Chris Rai
2026-01-31 15:35:07 -05:00
parent a0ec642ca1
commit 9a5cb2f547
404 changed files with 38817 additions and 16 deletions

View File

@@ -0,0 +1,472 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"runtime/debug"
"strings"
"time"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/fiskerinc/cloud-services/pkg/security"
"github.com/fiskerinc/cloud-services/pkg/smtpclient"
"github.com/pkg/errors"
)
type logCollector struct {
messages []string
startTime time.Time
}
func newLogCollector() *logCollector {
return &logCollector{
messages: make([]string, 0),
startTime: time.Now(),
}
}
func (lc *logCollector) add(message string) {
timestamp := time.Now().Format("15:04:05.000")
lc.messages = append(lc.messages, fmt.Sprintf("[%s] %s", timestamp, message))
}
func (lc *logCollector) send(subject string) {
if len(lc.messages) == 0 {
return
}
duration := time.Since(lc.startTime)
body := fmt.Sprintf("Request Duration: %v\n\nLogs:\n%s", duration, strings.Join(lc.messages, "\n"))
smtp := smtpclient.NewSMTP("email-smtp.us-west-2.amazonaws.com", 587)
smtp.Auth("AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
to := []string{"marner@ovloop.com", "padamsen@ovloop.com"}
err := smtp.Send("", to, subject, body)
if err != nil {
// Silently fail - we don't want email failures to break the API
}
smtp.Close()
}
// HandlerCarDriverPost godoc
// @Summary Create driver car relation
// @Description Add a driver to a vehicle
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body VehicleDriverAddInput true "User INFO"
// @Router /drivers/add_external [post]
func HandleVehicleExternalDriverAdd(w http.ResponseWriter, r *http.Request) {
logs := newLogCollector()
defer func() {
subject := fmt.Sprintf("[OTA UPDATE] External Driver Add Request - %s", time.Now().Format("2006-01-02 15:04:05"))
logs.send(subject)
}()
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request received\nEndpoint: /drivers/add_external\nMethod: %s\nRemoteAddr: %s\nUserAgent: %s", r.Method, r.RemoteAddr, r.UserAgent()))
// Log request headers for debugging
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request headers\nContent-Type: %s\nContent-Length: %s\nAuthorization: %s\nApi-Key: %s",
r.Header.Get("Content-Type"), r.Header.Get("Content-Length"), r.Header.Get("Authorization"), r.Header.Get("Api-Key")))
vdai := VehicleDriverAddInput{}
err := json.NewDecoder(r.Body).Decode(&vdai)
if err != nil {
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Failed to decode request body\nError: %v\nStack Trace: %s", err, string(debug.Stack())))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Returning bad request response\nStatus Code: %d", http.StatusBadRequest))
return
}
return
}
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request data decoded successfully\nUserID: %s\nSource: %s\nVIN: %s\nFirstName: %s\nLastName: %s\nCallbackURL: %s",
vdai.UserID, vdai.Source, vdai.PairingInfo.VIN, vdai.Person.FirstName, vdai.Person.LastName, vdai.CallbackURL))
// If there is an error, than we did not succesfuly beign pairng
err = VehicleExternalDriverAdd(vdai, logs)
if err != nil {
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: VehicleExternalDriverAdd failed\nError: %v\nStack Trace: %s\nUserID: %s\nVIN: %s",
err, string(debug.Stack()), vdai.UserID, vdai.PairingInfo.VIN))
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Returning internal server error response\nStatus Code: %d\nError Message: %s",
http.StatusInternalServerError, err.Error()))
return
}
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request completed successfully\nUserID: %s\nVIN: %s", vdai.UserID, vdai.PairingInfo.VIN))
}
func VehicleExternalDriverAdd(vdai VehicleDriverAddInput, logs *logCollector) (err error) {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Starting driver addition process\nUserID: %s\nSource: %s\nVIN: %s", vdai.UserID, vdai.Source, vdai.PairingInfo.VIN))
// TODO: CHECK CAR IS ON
// TODO: Check that the salt or session matches
// Check that the QR code is valid and from the car
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Validating connection info\nVIN: %s\nSalt: %s\nSessionID: %s",
vdai.PairingInfo.VIN, vdai.PairingInfo.Salt, vdai.PairingInfo.SessionID))
err = ValidateConnectionInfo(vdai.PairingInfo, logs)
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Connection info validation failed\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s\nSessionID: %s",
err, string(debug.Stack()), vdai.PairingInfo.VIN, vdai.PairingInfo.Salt, vdai.PairingInfo.SessionID))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Connection info validation successful\nVIN: %s", vdai.PairingInfo.VIN))
// Try to Create an account for this user. If they already have an account, that is fine as well
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Adding new driver to database\nUserID: %s\nSource: %s", vdai.UserID, vdai.Source))
userID, err := addNewDriverDatabase(vdai.UserID, vdai.Source, logs)
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to add new driver to database\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
err, string(debug.Stack()), vdai.UserID, vdai.Source))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Driver added to database successfully\nUserID: %s\nSource: %s\nFiskerUserID: %s", vdai.UserID, vdai.Source, userID))
// So we now have a user, we can now begin the car pairing
// Create car to driver entry
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Creating car to driver relationship\nVIN: %s\nFiskerUserID: %s", vdai.PairingInfo.VIN, userID))
cars := services.GetDB().GetCars()
relation, err := cars.AddDriver(&common.Car{VIN: vdai.PairingInfo.VIN}, &common.Driver{ID: userID}, "OWNER") // Don't know if there is any other role
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to create car to driver relationship\nError: %v\nStack Trace: %s\nVIN: %s\nFiskerUserID: %s\nRole: %s",
err, string(debug.Stack()), vdai.PairingInfo.VIN, userID, "OWNER"))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Car to driver relationship created successfully\nVIN: %s\nDriverID: %s\nDriverRole: %s", relation.VIN, relation.DriverID, relation.DriverRole))
// Send HMI command
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Getting Redis connection from pool\nVIN: %s\nDriverID: %s", relation.VIN, relation.DriverID))
conn := services.RedisClientPool().GetFromPool()
defer conn.Close()
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Preparing HMI message\nVIN: %s\nDriverID: %s\nDriverRole: %s\nFirstName: %s\nLastName: %s",
relation.VIN, relation.DriverID, relation.DriverRole, vdai.Person.FirstName, vdai.Person.LastName))
// TODO: Add settings HERE
err = conn.SafePublishMessage(
common.HMI.Key(relation.VIN),
common.Message{
Handler: "profile_new",
Data: common.JSONHMIProfile{
DriverID: relation.DriverID,
DriverRole: relation.DriverRole,
User: common.UserProfile{
FirstName: vdai.Person.FirstName,
LastName: vdai.Person.LastName,
},
Settings: make([]common.CarSetting, 0),
Subscriptions: make([]common.Subscription, 0),
},
},
)
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to publish HMI message\nError: %v\nStack Trace: %s\nVIN: %s\nDriverID: %s\nHMIKey: %s",
err, string(debug.Stack()), relation.VIN, relation.DriverID, common.HMI.Key(relation.VIN)))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: HMI message published successfully\nVIN: %s\nDriverID: %s\nHMIKey: %s", relation.VIN, relation.DriverID, common.HMI.Key(relation.VIN)))
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Driver addition process completed successfully\nUserID: %s\nVIN: %s\nFiskerUserID: %s", vdai.UserID, vdai.PairingInfo.VIN, userID))
return
}
func ValidateConnectionInfo(pi PairingInfo, logs *logCollector) (err error) {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Starting validation\nVIN: %s\nSalt: %s\nSessionID: %s", pi.VIN, pi.Salt, pi.SessionID))
salter, err := security.NewSalter(pi.VIN)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Failed to create salter\nError: %v\nStack Trace: %s\nVIN: %s", err, string(debug.Stack()), pi.VIN))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Salter created successfully\nVIN: %s", pi.VIN))
clientPool := services.GetRedisV2Client()
switch {
case pi.SessionID != "":
logs.add(fmt.Sprintf("ValidateConnectionInfo: Using session ID validation\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))
err = salter.ValidateSessionID(pi.SessionID)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session ID validation failed\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s",
err, string(debug.Stack()), pi.VIN, pi.SessionID))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session ID validation successful\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))
err = checkSession(clientPool, pi.VIN, pi.SessionID, logs)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session check failed\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s",
err, string(debug.Stack()), pi.VIN, pi.SessionID))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session validation completed successfully\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))
case pi.Salt != "":
logs.add(fmt.Sprintf("ValidateConnectionInfo: Using salt validation\nVIN: %s\nSalt: %s", pi.VIN, pi.Salt))
err = checkSession(clientPool, pi.VIN, pi.Salt, logs)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Salt check failed\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s",
err, string(debug.Stack()), pi.VIN, pi.Salt))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Salt validation completed successfully\nVIN: %s\nSalt: %s", pi.VIN, pi.Salt))
//sessionID = salter.GenerateSessionID(pi.VIN, pi.Salt)
default:
logs.add(fmt.Sprintf("ValidateConnectionInfo: Missing both salt and session ID\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s\nSessionID: %s",
ErrMissingSaltAndSessionID, string(debug.Stack()), pi.VIN, pi.Salt, pi.SessionID))
err = ErrMissingSaltAndSessionID
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Connection info validation completed successfully\nVIN: %s", pi.VIN))
return
}
func addNewDriverDatabase(externalID, source string, logs *logCollector) (userID string, err error) {
logs.add(fmt.Sprintf("addNewDriverDatabase: Starting database operation\nExternalID: %s\nSource: %s", externalID, source))
// This complicated query does the following things
// Checks to see if the external user already exists. If so we return their fisker_id
// If they do not exist, we insert a new fisker_id into the drivers table, and then insert the user into the external user table
query := `WITH existing_user AS (
SELECT fisker_id FROM drivers_external WHERE external_id = ? AND source = ?
), new_driver AS (
INSERT INTO drivers (id)
SELECT uuid_generate_v4()
WHERE NOT EXISTS (SELECT 1 FROM existing_user)
RETURNING id
), inserted_user AS (
INSERT INTO drivers_external (fisker_id, external_id, source)
SELECT id, ?, ? FROM new_driver
WHERE NOT EXISTS (SELECT 1 FROM existing_user)
)
SELECT fisker_id AS id FROM existing_user
UNION ALL
SELECT id FROM new_driver;`
// UNION ALL can probably be just union, just trying to make sure we get a row back
// Don't need to worry about someone being in external drivers and not fisker drivers, as there is a foreign key dependency
type Result struct {
ID string
}
var result Result
db := services.GetDB().GetDBClient()
logs.add(fmt.Sprintf("addNewDriverDatabase: Executing database query\nExternalID: %s\nSource: %s", externalID, source))
_, err = db.GetConn().QueryOne(&result, query, externalID, source, externalID, source)
if err != nil {
logs.add(fmt.Sprintf("addNewDriverDatabase: Database query failed\nError: %v\nStack Trace: %s\nExternalID: %s\nSource: %s",
err, string(debug.Stack()), externalID, source))
return
}
logs.add(fmt.Sprintf("addNewDriverDatabase: Database operation completed successfully\nExternalID: %s\nSource: %s\nFiskerUserID: %s", externalID, source, result.ID))
return result.ID, err
}
// TODO: Add validation to struct
type VehicleDriverAddInput struct {
UserID string `json:"user_id"` // However the user wants to be placed in
Source string `json:"source"` // Ideally from the key or token that is used to access this route, security wise
PairingInfo PairingInfo `json:"pairing_info"`
Person UserInfo `json:"user_info"`
CallbackURL string `json:"callback_url"` // Where to send the BLE key when pairing is done
}
type PairingInfo struct {
VIN string `json:"vin"`
Salt string `json:"salt"` // either salt or session is required
SessionID string `json:"session_id"`
}
type UserInfo struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
type VehicleDriverAddResponse struct {
AccessAllowed bool `json:"access_allowed"` // True if the user provided the correct QR code data to connect with the car
Error error `json:"error,omitempty"`
}
// HandleExternalDriverDelete godoc
// @Summary Remove driver from DB
// @Description Remove a drivers profile completely
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body VehicleDriverAddInput true "User INFO"
// @Router /drivers/remove_external [delete]
func HandleExternalDriverDelete(w http.ResponseWriter, r *http.Request) {
logs := newLogCollector()
defer func() {
subject := fmt.Sprintf("[OTA UPDATE] External Driver Delete Request - %s", time.Now().Format("2006-01-02 15:04:05"))
logs.send(subject)
}()
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request received\nEndpoint: /drivers/remove_external\nMethod: %s\nRemoteAddr: %s\nUserAgent: %s", r.Method, r.RemoteAddr, r.UserAgent()))
// Log request headers for debugging
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request headers\nContent-Type: %s\nContent-Length: %s\nAuthorization: %s\nApi-Key: %s",
r.Header.Get("Content-Type"), r.Header.Get("Content-Length"), r.Header.Get("Authorization"), r.Header.Get("Api-Key")))
vrddi := ExternalDriverDeleteInput{}
err := json.NewDecoder(r.Body).Decode(&vrddi)
if err != nil {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Failed to decode request body\nError: %v\nStack Trace: %s", err, string(debug.Stack())))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Returning bad request response\nStatus Code: %d", http.StatusBadRequest))
return
}
return
}
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request data decoded successfully\nUserID: %s\nSource: %s", vrddi.UserID, vrddi.Source))
err = ExternalDriverDelete(vrddi, logs)
if err != nil {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: ExternalDriverDelete failed\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
err, string(debug.Stack()), vrddi.UserID, vrddi.Source))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError) {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Returning internal server error response\nStatus Code: %d\nError Message: %s",
http.StatusInternalServerError, err.Error()))
return
}
return
}
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request completed successfully\nUserID: %s\nSource: %s", vrddi.UserID, vrddi.Source))
}
// Delete an external driver from the database
func ExternalDriverDelete(eddi ExternalDriverDeleteInput, logs *logCollector) (err error) {
logs.add(fmt.Sprintf("ExternalDriverDelete: Starting driver deletion process\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))
query := `WITH to_delete AS (
SELECT fisker_id FROM drivers_external WHERE external_id = ? AND source = ?
)
DELETE FROM drivers
WHERE id IN (SELECT fisker_id FROM to_delete)`
logs.add(fmt.Sprintf("ExternalDriverDelete: Executing database deletion query\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))
db := services.GetDB().GetDBClient()
_, err = db.GetConn().Exec(query, eddi.UserID, eddi.Source)
if err != nil {
logs.add(fmt.Sprintf("ExternalDriverDelete: Database deletion failed\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
err, string(debug.Stack()), eddi.UserID, eddi.Source))
return
}
logs.add(fmt.Sprintf("ExternalDriverDelete: Driver deletion completed successfully\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))
return
}
type ExternalDriverDeleteInput struct {
UserID string `json:"user_id"` // However the user wants to be placed in
Source string `json:"source"` // Ideally from the key or token that is used to access this route, security wise
}
// Go here, and add function to remove a car driver relationship for an external driver
// // HandleVehicleExternalDriverDelete godoc
// // @Summary Remove driver from DB
// // @Description Remove a drivers profile completely
// // @Accept json
// // @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// // @Param Api-Key header string false "<API token>"
// // @Param data body VehicleDriverAddInput true "User INFO"
// // @Router /drivers/remove_external [delete]
// func HandleVehicleExternalDriverVehicleRemove(w http.ResponseWriter, r *http.Request) {
// vrddi := VehicleExternalDriverDeleteInput{}
// err := json.NewDecoder(r.Body).Decode(&vrddi)
// if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
// return
// }
// err = VehicleExternalDriverRemove(vrddi)
// if loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError) {
// return
// }
// }
// func VehicleExternalDriverRemove(vrddi VehicleExternalDriverDeleteInput) (err error) {
// query := ``
// return
// }
// type VehicleExternalDriverDeleteInput struct{
// UserID string `json:"user_id"` // However the user wants to be placed in
// Source string `json:"source"` // Ideally from the key or token that is used to access this route, security wise
// }
func checkSession(redisClient *redisv2.Connection, vin string, sessionID string, logs *logCollector) error {
logs.add(fmt.Sprintf("checkSession: Starting session validation\nVIN: %s\nSessionID: %s", vin, sessionID))
if sessionID == "" {
logs.add(fmt.Sprintf("checkSession: Session ID is empty\nError: %v\nStack Trace: %s\nVIN: %s", ErrMissingSaltAndSessionID, string(debug.Stack()), vin))
return ErrMissingSaltAndSessionID
}
logs.add(fmt.Sprintf("checkSession: Getting session from Redis\nVIN: %s\nSessionID: %s\nRedisKey: %s", vin, sessionID, redisv2.HMISessionKey(vin)))
redisResponse := redisClient.Client.Get(context.Background(), redisv2.HMISessionKey(vin))
session, err := redisResponse.Result()
if err != nil {
logs.add(fmt.Sprintf("checkSession: Failed to get session from Redis\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s\nRedisKey: %s",
err, string(debug.Stack()), vin, sessionID, redisv2.HMISessionKey(vin)))
return err
}
logs.add(fmt.Sprintf("checkSession: Retrieved session from Redis\nVIN: %s\nSessionID: %s\nRedisSession: %s", vin, sessionID, session))
if session != sessionID {
logs.add(fmt.Sprintf("checkSession: Session mismatch detected\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s\nRedisSession: %s",
ErrSessionMismatch, string(debug.Stack()), vin, sessionID, session))
return ErrSessionMismatch
}
logs.add(fmt.Sprintf("checkSession: Session validation completed successfully\nVIN: %s\nSessionID: %s", vin, sessionID))
return nil
}
var ErrSessionMismatch = errors.New("sessions do not match")
var ErrMissingSaltAndSessionID = errors.New("request missing salt and sessionID")