package main import ( "database/sql" "encoding/json" "flag" "fmt" "math/rand" "net/http" "os" "path" "time" "github.com/gobuffalo/packr/v2" _ "github.com/mattn/go-sqlite3" "github.com/gorilla/mux" "github.com/jmoiron/sqlx" "github.com/rs/zerolog" "github.com/rs/zerolog/log" hashids "github.com/speps/go-hashids" "github.com/stretchr/graceful" ) var ( salt = flag.String("salt", "happy", "salt for IDs") minHashLen = flag.Int("minHashLen", 4, "minimum ID hash size") develop = flag.Bool("develop", false, "turn on develop mode") ) func main() { flag.Parse() log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr}) zerolog.SetGlobalLevel(zerolog.DebugLevel) box := packr.New("dist", "frontend/dist") log.Debug().Strs("dirlist", box.List()).Msg("packr made") s := server{ addr: "0.0.0.0:8080", assetPath: "pub", salt: *salt, box: box, } db, err := sqlx.Connect("sqlite3", "happy.db") if err != nil { log.Fatal(). Err(err). Msg("could not connect to database") } s.db = db hd := hashids.NewData() hd.Salt = s.salt hd.MinLength = *minHashLen s.h, _ = hashids.NewWithData(hd) if err := s.makeDB(); err != nil { log.Fatal(). Err(err). Msg("could not create database") } s.serve() } type server struct { addr string assetPath string db *sqlx.DB salt string h *hashids.HashID box *packr.Box } func (s *server) makeDB() error { q := `create table if not exists users ( id integer primary key, email text unique, verification integer not null, dateCreated integer, lastLogin integer )` if _, err := s.db.Exec(q); err != nil { return err } q = `create table if not exists mood_categories ( id integer primary key, name text )` if _, err := s.db.Exec(q); err != nil { return err } q = `create table if not exists mood_category_texts ( id integer primary key, mood_category_id integer, key text, value integer, foreign key(mood_category_id) references mood_categories(id) )` if _, err := s.db.Exec(q); err != nil { return err } q = `create table if not exists moods ( id integer primary key, user_id integer, mood_category_id integer, value integer, time integer, foreign key(user_id) references users(id), foreign key(mood_category_id) references mood_categories(id) )` if _, err := s.db.Exec(q); err != nil { return err } q = `select count(*) from mood_category_texts mct inner join mood_categories mc on mct.mood_category_id=mc.id` var count int if err := s.db.Get(&count, q); err != nil { return err } if count == 0 { return s.populateMoods() } return nil } func (s *server) populateMoods() error { tx := s.db.MustBegin() res := tx.MustExec(`insert into mood_categories (name) values ('happy')`) id, err := res.LastInsertId() if err != nil { tx.Rollback() return err } tx.MustExec(`insert into mood_category_texts (mood_category_id,key,value) values (?,?,?)`, id, "😄", 1) tx.MustExec(`insert into mood_category_texts (mood_category_id,key,value) values (?,?,?)`, id, "😐", 0) tx.MustExec(`insert into mood_category_texts (mood_category_id,key,value) values (?,?,?)`, id, "😟", -1) return tx.Commit() } func (s *server) recordMood(mood getMoodsResponse, who UserID) error { q := `insert into moods (user_id,mood_category_id,value,time) values (?,?,?,?)` _, err := s.db.Exec(q, who.ID, mood.CategoryID, mood.Value, time.Now().Unix()) return err } type UserID struct { db *sqlx.DB ID int64 Email sql.NullString Verification int64 DateCreated sql.NullInt64 `db:"dateCreated"` LastLogin sql.NullInt64 `db:"lastLogin"` salt int64 str string } func (s *server) NewUserID() (UserID, error) { uid := UserID{ db: s.db, DateCreated: sql.NullInt64{time.Now().Unix(), true}, LastLogin: sql.NullInt64{time.Now().Unix(), true}, Verification: rand.Int63(), } q := `insert into users (verification,dateCreated) values (?, ?)` res, err := s.db.Exec(q, uid.Verification, uid.DateCreated) if err != nil { return uid, err } id, err := res.LastInsertId() if err != nil { return uid, err } uid.ID = id idstr, err := s.h.EncodeInt64([]int64{id}) if err != nil { return uid, err } uid.str = idstr return uid, nil } func (s *server) FromStr(uid, verification string) (UserID, error) { id := UserID{db: s.db} if uid == "" || verification == "" { return id, fmt.Errorf("user ID and verification not given.") } idInt, err := s.h.DecodeInt64WithError(uid) if err != nil { return id, err } q := `select id,email,verification,datecreated,lastlogin from users where id=?` if err := s.db.Get(&id, q, idInt[0]); err != nil { log.Error(). Err(err). Msg("unable to select") return id, err } verify, err := s.h.EncodeInt64([]int64{id.Verification}) if err != nil { log.Error(). Err(err). Str("verify", verify). Str("id.Verification", verification). Msg("unable to encode") return id, err } if verify != verification { log.Debug(). Str("verify", verify). Str("id.Verification", verification). Msg("verification mismatch") return id, fmt.Errorf("Invalid verification token") } return id, nil } func (u UserID) String() string { return u.str } type RegisterResponse struct { ID string DateCreated int64 Validation string `json:",omitempty"` } func (s *server) handlerRegisterCode(w http.ResponseWriter, r *http.Request) { uid, err := s.NewUserID() if err != nil { w.WriteHeader(500) log.Error().Err(err).Msg("error from NewUserID") fmt.Fprintf(w, "Error registering user: %s", err) return } if err != nil { w.WriteHeader(500) log.Error().Err(err).Msg("error converting date") fmt.Fprintf(w, "Error registering user: %s", err) return } resp := RegisterResponse{ ID: uid.String(), DateCreated: uid.DateCreated.Int64, } if *develop { resp.Validation, _ = s.h.EncodeInt64([]int64{uid.Verification}) } out, err := json.Marshal(resp) if err != nil { w.WriteHeader(500) log.Error().Err(err).Msg("error from json.Marshal") fmt.Fprintf(w, "Error registering user: %s", err) return } fmt.Fprintf(w, "%s", string(out)) } type getMoodsResponse struct { CategoryID int64 `db:"category_id"` CategoryName string `db:"category_name"` Key string Value int64 } func (s *server) getMoods(w http.ResponseWriter, r *http.Request) { log.Debug().Interface("req", r).Msg("getMoods") q := `select mc.id category_id,mc.name category_name,mct.key,mct.value from mood_categories mc inner join mood_category_texts mct on mc.id=mct.mood_category_id` recs := []getMoodsResponse{} err := s.db.Select(&recs, q) if err != nil { log.Error().Err(err).Msg("could not retrieve mood categories and texts") w.WriteHeader(500) fmt.Fprintf(w, "Error getting moods: %s", err) return } resp, err := json.Marshal(recs) if err != nil { log.Error().Err(err).Msg("error from json.Marshal") w.WriteHeader(500) fmt.Fprintf(w, "Error getting moods: %s", err) return } fmt.Fprintf(w, "%s", string(resp)) } func (s *server) checkUser(w http.ResponseWriter, r *http.Request) { uid := r.Header.Get("X-user-id") verify := r.Header.Get("X-user-validation") log.Debug(). Str("uid", uid). Str("verify", verify). Msg("checkUser") user, err := s.FromStr(uid, verify) if err != nil { log.Error().Err(err).Msg("user not known") w.WriteHeader(http.StatusUnauthorized) fmt.Fprint(w, "User not known") return } j, err := json.Marshal(user) if err != nil { w.WriteHeader(500) log.Error().Err(err).Msg("could not marshal user") fmt.Fprintf(w, "%s", err) return } w.Write(j) } func (s *server) handleMood(w http.ResponseWriter, r *http.Request) { uid := r.Header.Get("X-user-id") verify := r.Header.Get("X-user-validation") log.Debug(). Str("uid", uid). Str("verify", verify). Msg("handleMood") dec := json.NewDecoder(r.Body) happyReq := getMoodsResponse{} err := dec.Decode(&happyReq) if err != nil { log.Error().Err(err).Msg("error with happy") w.WriteHeader(400) fmt.Fprintf(w, err.Error()) return } log.Debug(). Interface("mood", happyReq). Msg("mood") user, err := s.FromStr(uid, verify) if err != nil { log.Error(). Err(err). Msg("error getting user") w.WriteHeader(403) fmt.Fprintf(w, "Error: %s", err) return } if err := s.recordMood(happyReq, user); err != nil { log.Error().Err(err).Msg("error saving mood") w.WriteHeader(500) fmt.Fprintf(w, "Error: %s", err) return } fmt.Fprintf(w, "ok") } func (s *server) indexHandler(entryPoint string) func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) { p := path.Clean(r.URL.Path) if s.box.Has(p) && !s.box.HasDir(p) { f, err := s.box.Find(p) if err != nil { log.Error().Err(err).Msg("Error finding file") w.WriteHeader(http.StatusNotFound) } w.Write(f) return } if s.box.HasDir(p) && s.box.Has(path.Join(p, "index.html")) { f, err := s.box.Find(path.Join(p, "index.html")) if err != nil { log.Error().Err(err).Msg("Error finding file") w.WriteHeader(http.StatusNotFound) } w.Write(f) return } if f, err := s.box.Find(p); err != nil { w.Write(f) return } w.WriteHeader(http.StatusNotFound) } return fn } func (s *server) routeSetup() *mux.Router { r := mux.NewRouter() api := r.PathPrefix("/v1/").Subrouter() api.HandleFunc("/moods", s.getMoods).Methods("GET") api.HandleFunc("/moods", s.handleMood).Methods("POST") api.HandleFunc("/user/code", s.handlerRegisterCode).Methods("GET") api.HandleFunc("/user/info", s.checkUser).Methods("GET") r.PathPrefix("/").HandlerFunc(s.indexHandler("/index.html")) return r } func (s *server) serve() { middle := s.routeSetup() log.Info().Str("addr", "http://"+s.addr).Msg("serving HTTP") graceful.Run(s.addr, 10*time.Second, middle) }