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 " // @Param Api-Key header string false "" // @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 " // @Param Api-Key header string false "" // @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 " // // @Param Api-Key header string false "" // // @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")