From 9e3e1d72b54c394e1eae46f87d4f7536ecef135c Mon Sep 17 00:00:00 2001 From: larribas Date: Sun, 16 Oct 2016 17:38:41 +0200 Subject: [PATCH 1/4] Mock trips endpoints --- api/trips.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 2 ++ 2 files changed, 55 insertions(+) diff --git a/api/trips.go b/api/trips.go index 84a0fbf..cb4b5fa 100644 --- a/api/trips.go +++ b/api/trips.go @@ -40,8 +40,61 @@ func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { jsonTrip, err := json.Marshal(trip) if err != nil { log.Printf("Unexpected error %s when marshaling the trip into JSON", err.Error()) + w.WriteHeader(http.StatusInternalServerError) } w.WriteHeader(http.StatusCreated) w.Write([]byte(jsonTrip)) +} + + +func GetTripsList(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + /* + page, err := strconv.Atoi(r.URL.Query().Get("page")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("The page must be an integer")) + return + } + + pageSize, err := strconv.Atoi(r.URL.Query().Get("page_size")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("The page_size must be an integer")) + return + } + */ + + // TODO: Get all trips from the database, filtered and paginated + trip1, _ := models.NewTrip("sydney") + trip2, _ := models.NewTrip("ottawa") + trips := [2]*models.Trip{trip1, trip2} + + jsonTrips, err := json.Marshal(trips) + if err != nil { + log.Printf("Unexpected error %s when marshaling the trip into JSON", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(jsonTrips)) +} + + +func GetTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + id := ps.ByName("id") + fmt.Printf("id is %s", id) + + // TODO: Get from database + trip, _ := models.NewTrip("sydney") + jsonTrip, err := json.Marshal(trip) + if err != nil { + log.Printf("Unexpected error %s when marshaling the trip into JSON", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(jsonTrip)) } \ No newline at end of file diff --git a/main.go b/main.go index d076ed6..f0fcd19 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,9 @@ func main() { router := httprouter.New() router.GET("/", Ping) + router.GET("/trips", api.GetTripsList) router.POST("/trips", api.CreateTrip) + router.GET("/trips/:id", api.GetTrip) log.Println("Listening on 8080") log.Fatal(http.ListenAndServe(":8080", router)) From d3db2963ed92fdf12257acd0e9c1eadf1a5226b1 Mon Sep 17 00:00:00 2001 From: larribas Date: Sun, 16 Oct 2016 18:20:21 +0200 Subject: [PATCH 2/4] Implement configuration management and in-memoy trip repo --- adapters/trip_repo/in_memory.go | 50 +++++++++++++++++++++++++++++++++ api/trips.go | 32 +++++++++++---------- config/config.go | 7 +++++ main.go | 22 +++++++++++++-- 4 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 adapters/trip_repo/in_memory.go create mode 100644 config/config.go diff --git a/adapters/trip_repo/in_memory.go b/adapters/trip_repo/in_memory.go new file mode 100644 index 0000000..cc52398 --- /dev/null +++ b/adapters/trip_repo/in_memory.go @@ -0,0 +1,50 @@ +package trip_repo + +import ( + "github.com/devlucky/maporable-api/models" + "errors" +) + +type TripRepo interface { + List() ([]*models.Trip) + Get(id string) (*models.Trip) + Create(trip *models.Trip) (error) +} + +func Test() (TripRepo) { + return NewInMemory() +} + + +type InMemory struct { + trips map[string]*models.Trip +} + +func NewInMemory() (*InMemory) { + return &InMemory{ + trips: make(map[string]*models.Trip), + } +} + +func (repo *InMemory) List() ([]*models.Trip) { + list := make([]*models.Trip, 0, len(repo.trips)) + + for _, trip := range repo.trips { + list = append(list, trip) + } + + return list +} + +func (repo *InMemory) Get(id string) (*models.Trip) { + return repo.trips[id] +} + +func (repo *InMemory) Create(trip *models.Trip) (error) { + if _, ok := repo.trips[trip.Id]; ok { + return errors.New("Duplicate ID for trip") + } + + repo.trips[trip.Id] = trip + return nil +} \ No newline at end of file diff --git a/api/trips.go b/api/trips.go index cb4b5fa..0429407 100644 --- a/api/trips.go +++ b/api/trips.go @@ -7,13 +7,14 @@ import ( "encoding/json" "log" "fmt" + "github.com/devlucky/maporable-api/config" ) type CreateTripInput struct { Place string `json:"place"` } -func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { +func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Adapters) { var input CreateTripInput decoder := json.NewDecoder(r.Body) @@ -33,9 +34,12 @@ func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { return } - - // TODO: Save it - + err = a.TripRepo.Create(trip) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } jsonTrip, err := json.Marshal(trip) if err != nil { @@ -47,8 +51,7 @@ func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { w.Write([]byte(jsonTrip)) } - -func GetTripsList(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { +func GetTripsList(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Adapters) { /* page, err := strconv.Atoi(r.URL.Query().Get("page")) if err != nil { @@ -65,11 +68,7 @@ func GetTripsList(w http.ResponseWriter, r *http.Request, ps httprouter.Params) } */ - // TODO: Get all trips from the database, filtered and paginated - trip1, _ := models.NewTrip("sydney") - trip2, _ := models.NewTrip("ottawa") - trips := [2]*models.Trip{trip1, trip2} - + trips := a.TripRepo.List() jsonTrips, err := json.Marshal(trips) if err != nil { log.Printf("Unexpected error %s when marshaling the trip into JSON", err.Error()) @@ -82,12 +81,15 @@ func GetTripsList(w http.ResponseWriter, r *http.Request, ps httprouter.Params) } -func GetTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { +func GetTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Adapters) { id := ps.ByName("id") - fmt.Printf("id is %s", id) - // TODO: Get from database - trip, _ := models.NewTrip("sydney") + trip := a.TripRepo.Get(id) + if trip == nil { + w.WriteHeader(http.StatusNotFound) + return + } + jsonTrip, err := json.Marshal(trip) if err != nil { log.Printf("Unexpected error %s when marshaling the trip into JSON", err.Error()) diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..c4bda8d --- /dev/null +++ b/config/config.go @@ -0,0 +1,7 @@ +package config + +import "github.com/devlucky/maporable-api/adapters/trip_repo" + +type Adapters struct { + TripRepo trip_repo.TripRepo +} diff --git a/main.go b/main.go index f0fcd19..8e5cc71 100644 --- a/main.go +++ b/main.go @@ -6,19 +6,35 @@ import ( "log" "github.com/julienschmidt/httprouter" "github.com/devlucky/maporable-api/api" + "github.com/devlucky/maporable-api/adapters/trip_repo" + "github.com/devlucky/maporable-api/config" ) +// TODO: Move this to API func Ping(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { fmt.Fprint(w, "pong") } +func InjectConfig(a *config.Adapters, f func (http.ResponseWriter, *http.Request, httprouter.Params, *config.Adapters)) (func (http.ResponseWriter, *http.Request, httprouter.Params)) { + return func (w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + f(w, r, ps, a) + } +} + +func Config() (*config.Adapters) { + return &config.Adapters{ + TripRepo: trip_repo.NewInMemory(), + } +} + func main() { + config := Config() router := httprouter.New() router.GET("/", Ping) - router.GET("/trips", api.GetTripsList) - router.POST("/trips", api.CreateTrip) - router.GET("/trips/:id", api.GetTrip) + router.GET("/trips", InjectConfig(config, api.GetTripsList)) + router.POST("/trips", InjectConfig(config, api.CreateTrip)) + router.GET("/trips/:id", InjectConfig(config, api.GetTrip)) log.Println("Listening on 8080") log.Fatal(http.ListenAndServe(":8080", router)) From b0b5e89d21db12fc252a2596e14a6454e00db40a Mon Sep 17 00:00:00 2001 From: larribas Date: Sun, 16 Oct 2016 19:18:44 +0200 Subject: [PATCH 3/4] Set the right attributes for a trip --- api/trips.go | 24 ++---------------------- models/trip.go | 38 +++++++++++++++++++++++++++++--------- models/validators.go | 8 ++++++++ 3 files changed, 39 insertions(+), 31 deletions(-) create mode 100644 models/validators.go diff --git a/api/trips.go b/api/trips.go index 0429407..b876994 100644 --- a/api/trips.go +++ b/api/trips.go @@ -10,12 +10,8 @@ import ( "github.com/devlucky/maporable-api/config" ) -type CreateTripInput struct { - Place string `json:"place"` -} - func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Adapters) { - var input CreateTripInput + var input models.Trip decoder := json.NewDecoder(r.Body) err := decoder.Decode(&input) @@ -26,7 +22,7 @@ func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a } defer r.Body.Close() - trip, err := models.NewTrip(input.Place) + trip, err := models.NewTrip(input.User, input.Country, input.Status, input.StartDate, input.EndDate) if err != nil { w.WriteHeader(http.StatusBadRequest) msg := fmt.Sprintf("Invalid trip parameter: %s", err.Error()) @@ -52,22 +48,6 @@ func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a } func GetTripsList(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Adapters) { - /* - page, err := strconv.Atoi(r.URL.Query().Get("page")) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("The page must be an integer")) - return - } - - pageSize, err := strconv.Atoi(r.URL.Query().Get("page_size")) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("The page_size must be an integer")) - return - } - */ - trips := a.TripRepo.List() jsonTrips, err := json.Marshal(trips) if err != nil { diff --git a/models/trip.go b/models/trip.go index f0f9a89..20ea650 100644 --- a/models/trip.go +++ b/models/trip.go @@ -1,28 +1,48 @@ package models -import "errors" +import ( + "github.com/satori/go.uuid" +) // A trip represents a one-time visit to a particular country type Trip struct { Id string `json:"id"` User string `json:"user"` - Place string `json:"place"` + Country string `json:"country"` + Status string `json:"status"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` - Description string `json:"description"` } -func NewTrip(place string) (*Trip, error) { - if place == "" { - return nil, errors.New("The place cannot be empty") +func NewTrip(user, country, status, startDate, endDate string) (*Trip, error) { + err := validateDate(startDate) + if err != nil { + return nil, err + } + + err = validateDate(endDate) + if err != nil { + return nil, err } trip := &Trip{ - User: "hector", - Place: place, + Id: uuid.NewV4().String(), + User: user, + Country: country, + Status: status, + StartDate: startDate, + EndDate: endDate, Latitude: 40.40, Longitude: 50.50, - Description: "lots of sharks and groundhogs", } + return trip, nil } + +// TODO: Actual functionality +func getCoords(country string) (lat float64, long float64) { + lat, long = 40.40, 50.50 + return +} diff --git a/models/validators.go b/models/validators.go new file mode 100644 index 0000000..aff2565 --- /dev/null +++ b/models/validators.go @@ -0,0 +1,8 @@ +package models + +import "time" + +func validateDate(d string) (error) { + _, err := time.Parse(time.RFC3339, d) + return err +} \ No newline at end of file From e2ddc4b1357f524786632bb2af6e51543f311ead Mon Sep 17 00:00:00 2001 From: larribas Date: Tue, 25 Oct 2016 18:58:12 +0200 Subject: [PATCH 4/4] Start implementing facebook login --- .gitignore | 2 ++ api/login.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ api/trips.go | 9 ++++--- conf.ejson | 7 +++++ config/config.go | 13 ++++++--- main.go | 57 +++++++++++++++++++++++++++++++++++----- 6 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 api/login.go create mode 100644 conf.ejson diff --git a/.gitignore b/.gitignore index daf913b..8efd083 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ _obj _test +conf.json + # Architecture specific extensions/prefixes *.[568vq] [568vq].out diff --git a/api/login.go b/api/login.go new file mode 100644 index 0000000..a8086d4 --- /dev/null +++ b/api/login.go @@ -0,0 +1,68 @@ +package api + +import ( + "github.com/julienschmidt/httprouter" + "net/http" + "github.com/devlucky/maporable-api/models" + "encoding/json" + "log" + "fmt" + "github.com/devlucky/maporable-api/config" + "net/url" + "strings" + "golang.org/x/oauth2" + "io/ioutil" +) + +func Login(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Config) { + Url, err := url.Parse(a.FacebookOAuth.Endpoint.AuthURL) + if err != nil { + log.Fatal("Parse: ", err) + } + parameters := url.Values{} + parameters.Add("client_id", a.FacebookOAuth.ClientID) + parameters.Add("scope", strings.Join(a.FacebookOAuth.Scopes, " ")) + parameters.Add("redirect_uri", a.FacebookOAuth.RedirectURL) + parameters.Add("response_type", "code") + parameters.Add("state", a.FacebookOAuthState) + + Url.RawQuery = parameters.Encode() + http.Redirect(w, r, Url.String(), http.StatusTemporaryRedirect) +} + +func LoginWithFacebook(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Config) { + state := r.FormValue("state") + if state != a.FacebookOAuthState { + log.Printf("invalid oauth state, expected '%s', got '%s'\n", a.FacebookOAuthState, state) + w.WriteHeader(http.StatusBadRequest) + return + } + + code := r.FormValue("code") + token, err := a.FacebookOAuth.Exchange(oauth2.NoContext, code) + if err != nil { + fmt.Printf("oauthConf.Exchange() failed with '%s'\n", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + resp, err := http.Get("https://graph.facebook.com/me?access_token=" + + url.QueryEscape(token.AccessToken)) + if err != nil { + fmt.Printf("Get: %s\n", err) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + defer resp.Body.Close() + + response, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Printf("ReadAll: %s\n", err) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + log.Printf("parseResponseBody: %s\n", string(response)) + + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) +} diff --git a/api/trips.go b/api/trips.go index b876994..3534c04 100644 --- a/api/trips.go +++ b/api/trips.go @@ -10,7 +10,7 @@ import ( "github.com/devlucky/maporable-api/config" ) -func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Adapters) { +func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Config) { var input models.Trip decoder := json.NewDecoder(r.Body) @@ -43,11 +43,12 @@ func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a w.WriteHeader(http.StatusInternalServerError) } + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) w.Write([]byte(jsonTrip)) } -func GetTripsList(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Adapters) { +func GetTripsList(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Config) { trips := a.TripRepo.List() jsonTrips, err := json.Marshal(trips) if err != nil { @@ -56,12 +57,13 @@ func GetTripsList(w http.ResponseWriter, r *http.Request, ps httprouter.Params, return } + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(jsonTrips)) } -func GetTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Adapters) { +func GetTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Config) { id := ps.ByName("id") trip := a.TripRepo.Get(id) @@ -77,6 +79,7 @@ func GetTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *co return } + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(jsonTrip)) } \ No newline at end of file diff --git a/conf.ejson b/conf.ejson new file mode 100644 index 0000000..2ecc898 --- /dev/null +++ b/conf.ejson @@ -0,0 +1,7 @@ +{ + "_public_key": "e75de5fc546d36ee5aa8db5afaaccf4d81f85fd73f13fa42ac35755d58ec4474", + "_fb_client_id": "1612792319017954", + "fb_client_secret": "EJ[1:sNEwNSlfmiJCVksoGlqW9tr/IX4aYkVGzohgQ5fjjgA=:1ylVW6Jpt2rA1HjR3Vtv7QiO59zhW8wV:Xk+t9GlZ1puMfalx1NViafiXZ/6uDbvDpC03uaqdizzCcOC2QrMaCPwZKI4QB5YS]", + "_fb_redirect_url": "localhost:8080" + "_fb_state_string": "r6ctng3iyfhumowai73hrm" +} \ No newline at end of file diff --git a/config/config.go b/config/config.go index c4bda8d..0155fcb 100644 --- a/config/config.go +++ b/config/config.go @@ -1,7 +1,14 @@ package config -import "github.com/devlucky/maporable-api/adapters/trip_repo" +import ( + "github.com/devlucky/maporable-api/adapters/trip_repo" + "golang.org/x/oauth2" +) -type Adapters struct { - TripRepo trip_repo.TripRepo +type Config struct { + TripRepo trip_repo.TripRepo + + // Facebook OAuth + FacebookOAuth *oauth2.Config + FacebookOAuthState string } diff --git a/main.go b/main.go index 8e5cc71..1d2d937 100644 --- a/main.go +++ b/main.go @@ -8,33 +8,76 @@ import ( "github.com/devlucky/maporable-api/api" "github.com/devlucky/maporable-api/adapters/trip_repo" "github.com/devlucky/maporable-api/config" + "golang.org/x/oauth2" + "golang.org/x/oauth2/facebook" + "encoding/json" + "io/ioutil" ) +const userConfFilename string = "conf.json" + // TODO: Move this to API func Ping(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { fmt.Fprint(w, "pong") } -func InjectConfig(a *config.Adapters, f func (http.ResponseWriter, *http.Request, httprouter.Params, *config.Adapters)) (func (http.ResponseWriter, *http.Request, httprouter.Params)) { +func InjectConfig(a *config.Config, f func (http.ResponseWriter, *http.Request, httprouter.Params, *config.Config)) (func (http.ResponseWriter, *http.Request, httprouter.Params)) { return func (w http.ResponseWriter, r *http.Request, ps httprouter.Params) { f(w, r, ps, a) } } -func Config() (*config.Adapters) { - return &config.Adapters{ +type UserConfig struct { + FbClientID string `json:"_fb_client_id"` + FbClientSecret string `json:"fb_client_secret"` + FbRedirectURL string `json:"_fb_redirect_url"` + FbState string `json:"_fb_state "` +} + +func GetConfigVars() (*UserConfig) { + var uConf UserConfig + conf, err := ioutil.ReadFile(userConfFilename) + if err != nil { + log.Fatalf("Error reading from file %s", userConfFilename) + } + + err = json.Unmarshal(conf, uConf) + if err != nil { + log.Fatalf("Error unmarshaling conf in %s", userConfFilename) + } + + return uConf +} + +func CurrentConfig(uConf *UserConfig) (*config.Config) { + return &config.Config{ TripRepo: trip_repo.NewInMemory(), + FacebookOAuth: &oauth2.Config{ + ClientID: uConf.FbClientID, + ClientSecret: uConf.FbClientSecret, + RedirectURL: uConf.FbRedirectURL, + Scopes: []string{"public_profile"}, + Endpoint: facebook.Endpoint, + }, + FacebookOAuthState: uCong.FbState, } } func main() { - config := Config() + conf := CurrentConfig(GetConfigVars()) router := httprouter.New() + + // Ping-pong router.GET("/", Ping) - router.GET("/trips", InjectConfig(config, api.GetTripsList)) - router.POST("/trips", InjectConfig(config, api.CreateTrip)) - router.GET("/trips/:id", InjectConfig(config, api.GetTrip)) + // Authentication endpoints + router.POST("/login", InjectConfig(conf, api.Login)) + router.POST("/login/facebook", InjectConfig(conf, api.LoginWithFacebook)) + + // Trips endpoints + router.GET("/trips", InjectConfig(conf, api.GetTripsList)) + router.POST("/trips", InjectConfig(conf, api.CreateTrip)) + router.GET("/trips/:id", InjectConfig(conf, api.GetTrip)) log.Println("Listening on 8080") log.Fatal(http.ListenAndServe(":8080", router))