Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
_obj
_test

conf.json

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
Expand Down
50 changes: 50 additions & 0 deletions adapters/trip_repo/in_memory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package trip_repo
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK it's not recommended to use _ within package name, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. It's not very Go-y. What package structure and names would you use?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following go coding style I'd say triprepo


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
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a new line missing at the end of some of your files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be I didn't even go fmt the whole thing

68 changes: 68 additions & 0 deletions api/login.go
Original file line number Diff line number Diff line change
@@ -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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this endpoint. Could you detail it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our idea is to allow 3rd-party login via Facebook, right? This endpoint is used by the client to request the user to log in. It redirects the user to a Facebook login page with our app's information. When the user clicks on "accept" or "reject", Facebook calls the LoginWithFacebook callback with an access token corresponding to the user, and then we can get their data (email, name, ...)

One of the main ideas of this is that in the future maybe we can parse the metadata of pictures to create an interactive map of where they've been and with whom.

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)
}
56 changes: 47 additions & 9 deletions api/trips.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -25,23 +22,64 @@ 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())
w.Write([]byte(msg))
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))
}
7 changes: 7 additions & 0 deletions conf.ejson
Original file line number Diff line number Diff line change
@@ -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"
}
14 changes: 14 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -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
}
63 changes: 62 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 😁

}

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))
Expand Down
38 changes: 29 additions & 9 deletions models/trip.go
Original file line number Diff line number Diff line change
@@ -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"`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be a Country struct at some point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Everything you see here is very WIP at the moment. Our idea was to use an external API for countries, but I'm not sure yet which options we would have

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
}
8 changes: 8 additions & 0 deletions models/validators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package models
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather move this into an input/api validator rather than a models validator. I'd even put it into a parser module and return the actual Time rather than using a string within the model.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good :)


import "time"

func validateDate(d string) (error) {
_, err := time.Parse(time.RFC3339, d)
return err
}