package main import ( "encoding/json" "flag" "fmt" "net/http" "os" "path" "strconv" "time" packr "github.com/gobuffalo/packr/v2" "code.chrissexton.org/cws/happy/email" "code.chrissexton.org/cws/happy/user" _ "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") mailAddr = flag.String("mailAddr", "", "email server address") mailPort = flag.Int("mailPort", 587, "email server port") mailUser = flag.String("mailUser", "", "email user") mailPass = flag.String("mailPass", "", "email password") ) func validateMail() bool { log.Debug().Str("mailAddr", *mailAddr).Msg("about to look up mail") if val, ok := os.LookupEnv("MAIL_ADDR"); *mailAddr == "" && ok { *mailAddr = val log.Debug().Str("addr", *mailAddr).Str("val", val).Msg("set mailAddr") } if val, ok := os.LookupEnv("MAIL_PORT"); *mailPort == 0 && ok { *mailPort, _ = strconv.Atoi(val) log.Debug().Int("val", *mailPort).Msg("set mailPort") } if val, ok := os.LookupEnv("MAIL_USER"); *mailUser == "" && ok { *mailUser = val log.Debug().Str("val", *mailUser).Msg("set mailUser") } if val, ok := os.LookupEnv("MAIL_PASS"); *mailPass == "" && ok { *mailPass = val log.Debug().Str("val", *mailPass).Msg("set mailPass") } if *mailAddr != "" && *mailPort != 0 && *mailUser != "" && *mailPass != "" { return true } return false } 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") } if validateMail() { log.Debug().Msg("sending mail") s.email = email.New(*mailAddr, *mailPort, *mailUser, *mailPass) u, _ := s.NewUser() s.email.SendNewUserMail("chris@chrissexton.org", u, "http://happy.chrissexton.org") } else { log.Debug().Msg("mail disabled") } s.serve() } type server struct { addr string assetPath string db *sqlx.DB salt string h *hashids.HashID box *packr.Box email *email.EMailClient } 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 user.User) 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 } func (s *server) NewUser() (user.User, error) { uid := user.New(s.db, s.salt, s.h) 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 return uid, nil } func (s *server) FromStr(uid, verification string) (user.User, error) { id := user.New(s.db, s.salt, s.h) 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 } type RegisterResponse struct { ID string DateCreated int64 Validation string `json:",omitempty"` } func (s *server) handlerRegisterCode(w http.ResponseWriter, r *http.Request) { uid, err := s.NewUser() 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) }