go: refactor

* Move web stuff out to respective files
* Move DB stuff to its own package
* Move user stuff to its own package
This commit is contained in:
Chris Sexton 2019-10-29 01:19:38 -04:00
parent b5ed7342a0
commit ed411d5a5c
8 changed files with 422 additions and 347 deletions

84
db/db.go Normal file
View File

@ -0,0 +1,84 @@
package db
import "github.com/jmoiron/sqlx"
type Database struct {
*sqlx.DB
}
func New(path string) (*Database, error) {
d, err := sqlx.Open("sqlite3", path)
if err != nil {
return nil, err
}
return &Database{d}, nil
}
func (d *Database) 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 := d.Exec(q); err != nil {
return err
}
q = `create table if not exists mood_categories (
id integer primary key,
name text
)`
if _, err := d.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 := d.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 := d.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 := d.Get(&count, q); err != nil {
return err
}
if count == 0 {
return d.populateMoods()
}
return nil
}
func (d *Database) populateMoods() error {
tx := d.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()
}

362
serve.go
View File

@ -1,31 +1,28 @@
package main package main
import ( import (
"encoding/json"
"flag" "flag"
"fmt"
"net/http"
"os" "os"
"path"
"strconv" "strconv"
"time"
"code.chrissexton.org/cws/happy/web"
"code.chrissexton.org/cws/happy/db"
packr "github.com/gobuffalo/packr/v2" packr "github.com/gobuffalo/packr/v2"
"code.chrissexton.org/cws/happy/email" "code.chrissexton.org/cws/happy/email"
"code.chrissexton.org/cws/happy/user"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
hashids "github.com/speps/go-hashids" hashids "github.com/speps/go-hashids"
"github.com/stretchr/graceful"
) )
var ( var (
dbPath = flag.String("db", "happy.db", "path to db")
httpAddr = flag.String("httpAddr", "0.0.0.0:8080", "http address")
salt = flag.String("salt", "happy", "salt for IDs") salt = flag.String("salt", "happy", "salt for IDs")
minHashLen = flag.Int("minHashLen", 4, "minimum ID hash size") minHashLen = flag.Int("minHashLen", 4, "minimum ID hash size")
develop = flag.Bool("develop", false, "turn on develop mode") develop = flag.Bool("develop", false, "turn on develop mode")
@ -67,350 +64,29 @@ func main() {
box := packr.New("dist", "frontend/dist") box := packr.New("dist", "frontend/dist")
log.Debug().Strs("dirlist", box.List()).Msg("packr made") log.Debug().Strs("dirlist", box.List()).Msg("packr made")
s := server{ db, err := db.New(*dbPath)
addr: "0.0.0.0:8080",
assetPath: "pub",
salt: *salt,
box: box,
}
db, err := sqlx.Connect("sqlite3", "happy.db")
if err != nil { if err != nil {
log.Fatal(). log.Fatal().
Err(err). Err(err).
Msg("could not connect to database") Msg("could not connect to database")
} }
s.db = db
hd := hashids.NewData() hd := hashids.NewData()
hd.Salt = s.salt hd.Salt = *salt
hd.MinLength = *minHashLen hd.MinLength = *minHashLen
s.h, _ = hashids.NewWithData(hd) h, _ := hashids.NewWithData(hd)
if err := s.makeDB(); err != nil {
log.Fatal(). var mailClient *email.EMailClient
Err(err).
Msg("could not create database")
}
if validateMail() { if validateMail() {
log.Debug().Msg("sending mail") log.Debug().Msg("sending mail")
s.email = email.New(*mailAddr, *mailPort, *mailUser, *mailPass) mailClient = email.New(*mailAddr, *mailPort, *mailUser, *mailPass)
u, _ := s.NewUser()
s.email.SendNewUserMail("chris@chrissexton.org", u, "http://happy.chrissexton.org")
} else { } else {
log.Debug().Msg("mail disabled") log.Debug().Msg("mail disabled")
} }
s.serve() s := web.New(*httpAddr, "pub", db, *salt, h, box, mailClient)
} if mailClient != nil {
u, _ := s.NewUser()
type server struct { mailClient.SendNewUserMail("chris@chrissexton.org", u, "http://happy.chrissexton.org")
addr string }
assetPath string s.Serve()
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)
} }

View File

@ -5,13 +5,13 @@ import (
"math/rand" "math/rand"
"time" "time"
"github.com/speps/go-hashids" "code.chrissexton.org/cws/happy/db"
"github.com/jmoiron/sqlx" "github.com/speps/go-hashids"
) )
type User struct { type User struct {
db *sqlx.DB db *db.Database
ID int64 ID int64
Email sql.NullString Email sql.NullString
@ -23,7 +23,7 @@ type User struct {
str string str string
} }
func New(db *sqlx.DB, salt string, h *hashids.HashID) User { func New(db *db.Database, salt string, h *hashids.HashID) User {
u := User{ u := User{
db: db, db: db,
salt: salt, salt: salt,

77
web/mood_handlers.go Normal file
View File

@ -0,0 +1,77 @@
package web
import (
"encoding/json"
"fmt"
"net/http"
"github.com/rs/zerolog/log"
)
type getMoodsResponse struct {
CategoryID int64 `db:"category_id"`
CategoryName string `db:"category_name"`
Key string
Value int64
}
func (web *Web) 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 := web.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 (web *Web) 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 := web.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 := web.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")
}

60
web/routes.go Normal file
View File

@ -0,0 +1,60 @@
package web
import (
"time"
packr "github.com/gobuffalo/packr/v2"
"code.chrissexton.org/cws/happy/email"
"github.com/speps/go-hashids"
"code.chrissexton.org/cws/happy/db"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"github.com/stretchr/graceful"
)
type Web struct {
addr string
assetPath string
db *db.Database
salt string
h *hashids.HashID
box *packr.Box
email *email.EMailClient
}
func New(addr, assetPath string, db *db.Database, salt string, h *hashids.HashID, box *packr.Box, email *email.EMailClient) *Web {
w := &Web{
addr: addr,
assetPath: assetPath,
db: db,
salt: salt,
h: h,
box: box,
email: email,
}
if err := db.MakeDB(); err != nil {
log.Fatal().
Err(err).
Msg("could not create database")
}
return w
}
func (web *Web) routeSetup() *mux.Router {
r := mux.NewRouter()
api := r.PathPrefix("/v1/").Subrouter()
api.HandleFunc("/moods", web.getMoods).Methods("GET")
api.HandleFunc("/moods", web.handleMood).Methods("POST")
api.HandleFunc("/user/code", web.handlerRegisterCode).Methods("GET")
api.HandleFunc("/user/info", web.checkUser).Methods("GET")
r.PathPrefix("/").HandlerFunc(web.indexHandler("/index.html"))
return r
}
func (web *Web) Serve() {
middle := web.routeSetup()
log.Info().Str("addr", "http://"+web.addr).Msg("serving HTTP")
graceful.Run(web.addr, 10*time.Second, middle)
}

65
web/user.go Normal file
View File

@ -0,0 +1,65 @@
package web
import (
"fmt"
"time"
"code.chrissexton.org/cws/happy/user"
"github.com/rs/zerolog/log"
)
func (web *Web) recordMood(mood getMoodsResponse, who user.User) error {
q := `insert into moods (user_id,mood_category_id,value,time) values (?,?,?,?)`
_, err := web.db.Exec(q, who.ID, mood.CategoryID, mood.Value, time.Now().Unix())
return err
}
func (web *Web) NewUser() (user.User, error) {
uid := user.New(web.db, web.salt, web.h)
q := `insert into users (verification,dateCreated) values (?, ?)`
res, err := web.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 (web *Web) FromStr(uid, verification string) (user.User, error) {
id := user.New(web.db, web.salt, web.h)
if uid == "" || verification == "" {
return id, fmt.Errorf("user ID and verification not given.")
}
idInt, err := web.h.DecodeInt64WithError(uid)
if err != nil {
return id, err
}
q := `select id,email,verification,datecreated,lastlogin from users where id=?`
if err := web.db.Get(&id, q, idInt[0]); err != nil {
log.Error().
Err(err).
Msg("unable to select")
return id, err
}
verify, err := web.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
}

72
web/user_handlers.go Normal file
View File

@ -0,0 +1,72 @@
package web
import (
"encoding/json"
"fmt"
"net/http"
"github.com/rs/zerolog/log"
)
var Develop = false
type RegisterResponse struct {
ID string
DateCreated int64
Verification string `json:",omitempty"`
}
func (web *Web) handlerRegisterCode(w http.ResponseWriter, r *http.Request) {
uid, err := web.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.Verification, _ = web.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))
}
func (web *Web) 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 := web.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)
}

41
web/web_handlers.go Normal file
View File

@ -0,0 +1,41 @@
package web
import (
"net/http"
"path"
"github.com/rs/zerolog/log"
)
func (web *Web) 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 web.box.Has(p) && !web.box.HasDir(p) {
f, err := web.box.Find(p)
if err != nil {
log.Error().Err(err).Msg("Error finding file")
w.WriteHeader(http.StatusNotFound)
}
w.Write(f)
return
}
if web.box.HasDir(p) && web.box.Has(path.Join(p, "index.html")) {
f, err := web.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 := web.box.Find(p); err != nil {
w.Write(f)
return
}
w.WriteHeader(http.StatusNotFound)
}
return fn
}