happy/serve.go

405 lines
9.7 KiB
Go

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)
}