happy/serve.go

347 lines
8.2 KiB
Go

package main
import (
"database/sql"
"encoding/json"
"flag"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"path"
"time"
"github.com/chrissexton/happy/data"
"github.com/speps/go-hashids"
_ "github.com/mattn/go-sqlite3"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/stretchr/graceful"
)
var (
distPath = flag.String("dist", "/", "path to dist files")
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)
s := server{
addr: "0.0.0.0:8080",
assetPath: "pub",
salt: *salt,
}
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
}
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
}
return nil
}
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}
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) 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.Join(*distPath, path.Clean(r.URL.Path))
f, err := data.Assets.Open(p)
if err == nil {
if finfo, err := f.Stat(); err != nil && !finfo.IsDir() {
io.Copy(w, f)
return
}
}
log.Debug().Err(err).Str("path", p).Msg("file not found")
p = path.Join(p, "index.html")
f, err = data.Assets.Open(p)
if err == nil {
if finfo, err := f.Stat(); err != nil && !finfo.IsDir() {
io.Copy(w, f)
return
}
}
log.Debug().Err(err).Str("path", p).Msg("file not found")
p = path.Join(*distPath, entryPoint)
f, err = data.Assets.Open(p)
if err != nil {
log.Debug().Err(err).Str("path", p).Msg("file not found")
w.WriteHeader(http.StatusNotFound)
return
}
io.Copy(w, f)
}
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("/register/code", s.handlerRegisterCode).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)
}