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/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/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 84a0fbf..3534c04 100644 --- a/api/trips.go +++ b/api/trips.go @@ -7,14 +7,11 @@ 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) { - var input CreateTripInput +func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params, a *config.Config) { + var input models.Trip decoder := json.NewDecoder(r.Body) err := decoder.Decode(&input) @@ -25,7 +22,7 @@ func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { } 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()) @@ -33,15 +30,56 @@ func CreateTrip(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { return } + err = a.TripRepo.Create(trip) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } - // TODO: Save it + 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.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.Config) { + 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()) + w.WriteHeader(http.StatusInternalServerError) + 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.Config) { + id := ps.ByName("id") + + 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()) + w.WriteHeader(http.StatusInternalServerError) + return } - w.WriteHeader(http.StatusCreated) + 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 new file mode 100644 index 0000000..0155fcb --- /dev/null +++ b/config/config.go @@ -0,0 +1,14 @@ +package config + +import ( + "github.com/devlucky/maporable-api/adapters/trip_repo" + "golang.org/x/oauth2" +) + +type Config struct { + TripRepo trip_repo.TripRepo + + // Facebook OAuth + FacebookOAuth *oauth2.Config + FacebookOAuthState string +} diff --git a/main.go b/main.go index d076ed6..1d2d937 100644 --- a/main.go +++ b/main.go @@ -6,17 +6,78 @@ 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" + "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.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) + } +} + +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() { + conf := CurrentConfig(GetConfigVars()) router := httprouter.New() + + // Ping-pong router.GET("/", Ping) - router.POST("/trips", api.CreateTrip) + // 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)) 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