From 0f393d2fbb1cec213948fd2429b42a3715d4394e Mon Sep 17 00:00:00 2001 From: Aaron Lindsay Date: Thu, 25 Jun 2015 22:36:58 -0400 Subject: [PATCH] Initial commit --- .gitignore | 1 + accounts.go | 21 +++++ db.go | 32 ++++++++ errors.go | 36 +++++++++ main.go | 77 ++++++++++++++++++ securities.go | 19 +++++ sessions.go | 118 ++++++++++++++++++++++++++++ transactions.go | 33 ++++++++ users.go | 203 ++++++++++++++++++++++++++++++++++++++++++++++++ util.go | 17 ++++ 10 files changed, 557 insertions(+) create mode 100644 .gitignore create mode 100644 accounts.go create mode 100644 db.go create mode 100644 errors.go create mode 100644 main.go create mode 100644 securities.go create mode 100644 sessions.go create mode 100644 transactions.go create mode 100644 users.go create mode 100644 util.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/accounts.go b/accounts.go new file mode 100644 index 0000000..6ec7205 --- /dev/null +++ b/accounts.go @@ -0,0 +1,21 @@ +package main + +type AccountType int64 + +const ( + Bank AccountType = 1 + Cash = 2 + Asset = 3 + Liability = 4 + Investment = 5 + Income = 6 + Expense = 7 +) + +type Account struct { + AccountId int64 + UserId int64 + SecurityId int64 + Type AccountType + Name string +} diff --git a/db.go b/db.go new file mode 100644 index 0000000..0c47090 --- /dev/null +++ b/db.go @@ -0,0 +1,32 @@ +package main + +import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" + "gopkg.in/gorp.v1" + "log" +) + +var DB *gorp.DbMap = initDB() + +func initDB() *gorp.DbMap { + db, err := sql.Open("sqlite3", "file:moneygo.sqlite?cache=shared&mode=rwc") + if err != nil { + log.Fatal(err) + } + + dbmap := &gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} + dbmap.AddTableWithName(User{}, "users").SetKeys(true, "UserId") + dbmap.AddTableWithName(Session{}, "sessions").SetKeys(true, "SessionId") + dbmap.AddTableWithName(Account{}, "accounts").SetKeys(true, "AccountId") + dbmap.AddTableWithName(Security{}, "security").SetKeys(true, "SecurityId") + dbmap.AddTableWithName(Transaction{}, "transactions").SetKeys(true, "TransactionId") + dbmap.AddTableWithName(Split{}, "splits").SetKeys(true, "SplitId") + + err = dbmap.CreateTablesIfNotExists() + if err != nil { + log.Fatal(err) + } + + return dbmap +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..206404b --- /dev/null +++ b/errors.go @@ -0,0 +1,36 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +type Error struct { + ErrorId int + ErrorString string +} + +var error_codes = map[int]string{ + 1: "Not Signed In", + 2: "Unauthorized Access", + 3: "Invalid Request", + 4: "User Exists", + // 5: "Connection Failed", //client-side error + 999: "Internal Error", +} + +func WriteError(w http.ResponseWriter, error_code int) { + msg, ok := error_codes[error_code] + if !ok { + log.Printf("Error: WriteError received error code of %d", error_code) + msg = error_codes[999] + } + e := Error{error_code, msg} + + enc := json.NewEncoder(w) + err := enc.Encode(e) + if err != nil { + log.Fatal(err) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..6a8389a --- /dev/null +++ b/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "flag" + "github.com/gorilla/context" + "log" + "net" + "net/http" + "net/http/fcgi" + "os" + "path" + "strconv" +) + +var serveFcgi bool +var baseDir string +var port int +var smtpServer string +var smtpPort int +var smtpUsername string +var smtpPassword string +var reminderEmail string + +func init() { + flag.StringVar(&baseDir, "base", "./", "Base directory for server") + flag.IntVar(&port, "port", 80, "Port to serve API/files on") + flag.StringVar(&smtpServer, "smtp.server", "smtp.example.com", "SMTP server to send reminder emails from.") + flag.IntVar(&smtpPort, "smtp.port", 587, "SMTP server port to connect to") + flag.StringVar(&smtpUsername, "smtp.username", "moneygo", "SMTP username") + flag.StringVar(&smtpPassword, "smtp.password", "password", "SMTP password") + flag.StringVar(&reminderEmail, "email", "moneygo@example.com", "Email address to send reminder emails as.") + flag.BoolVar(&serveFcgi, "fcgi", false, "Serve via fcgi rather than http.") + flag.Parse() + + static_path := path.Join(baseDir, "static") + + // Ensure base directory is valid + dir_err_str := "The base directory doesn't look like it contains the " + + "'static' directory. Check to make sure you're passing the right " + + "value to the -base argument." + static_dir, err := os.Stat(static_path) + if err != nil { + log.Print(err) + log.Fatal(dir_err_str) + } + if !static_dir.IsDir() { + log.Fatal(dir_err_str) + } +} + +func rootHandler(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, path.Join(baseDir, "static/index.html")) +} + +func staticHandler(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, path.Join(baseDir, r.URL.Path)) +} + +func main() { + servemux := http.NewServeMux() + servemux.HandleFunc("/", rootHandler) + servemux.HandleFunc("/static/", staticHandler) + servemux.HandleFunc("/session/", SessionHandler) + servemux.HandleFunc("/user/", UserHandler) + + listener, err := net.Listen("tcp", ":"+strconv.Itoa(port)) + if err != nil { + log.Fatal(err) + } + + log.Printf("Serving on port %d out of directory: %s", port, baseDir) + if serveFcgi { + fcgi.Serve(listener, context.ClearHandler(servemux)) + } else { + http.Serve(listener, context.ClearHandler(servemux)) + } +} diff --git a/securities.go b/securities.go new file mode 100644 index 0000000..9790b38 --- /dev/null +++ b/securities.go @@ -0,0 +1,19 @@ +package main + +type SecurityType int64 + +const ( + Banknote SecurityType = 1 + Bond = 2 + Stock = 3 + MutualFund = 4 +) + +type Security struct { + SecurityId int64 + Name string + // Number of decimal digits (to the right of the decimal point) this + // security is precise to + Precision int64 + Type SecurityType +} diff --git a/sessions.go b/sessions.go new file mode 100644 index 0000000..310ac20 --- /dev/null +++ b/sessions.go @@ -0,0 +1,118 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" + "net/http" +) + +var cookie_store = sessions.NewCookieStore(securecookie.GenerateRandomKey(64)) + +type Session struct { + SessionId int64 + SessionSecret string `json:"-"` + UserId int64 +} + +func (s *Session) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(s) +} + +func GetSession(r *http.Request) (*Session, error) { + var s Session + + session, _ := cookie_store.Get(r, "moneygo") + _, ok := session.Values["session-secret"] + if !ok { + return nil, fmt.Errorf("session-secret cookie not set") + } + s.SessionSecret = session.Values["session-secret"].(string) + + err := DB.SelectOne(&s, "SELECT * from sessions where SessionSecret=?", s.SessionSecret) + if err != nil { + return nil, err + } + return &s, nil +} + +func DeleteSessionIfExists(r *http.Request) { + session, err := GetSession(r) + if err == nil { + DB.Delete(session) + } +} + +func NewSession(w http.ResponseWriter, r *http.Request, userid int64) (*Session, error) { + s := Session{} + + session, _ := cookie_store.Get(r, "moneygo") + + session.Values["session-secret"] = string(securecookie.GenerateRandomKey(64)) + s.SessionSecret = session.Values["session-secret"].(string) + s.UserId = userid + + err := DB.Insert(&s) + if err != nil { + return nil, err + } + + err = session.Save(r, w) + if err != nil { + return nil, err + } else { + return &s, nil + } +} + +func SessionHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" || r.Method == "PUT" { + user_json := r.PostFormValue("user") + if user_json == "" { + WriteError(w, 3 /*Invalid Request*/) + return + } + + user := User{} + err := user.Read(user_json) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + + dbuser, err := GetUserByUsername(user.Username) + if err != nil { + WriteError(w, 2 /*Unauthorized Access*/) + return + } + + user.HashPassword() + if user.PasswordHash != dbuser.PasswordHash { + WriteError(w, 2 /*Unauthorized Access*/) + return + } + + DeleteSessionIfExists(r) + + _, err = NewSession(w, r, dbuser.UserId) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + return + } + + WriteSuccess(w) + } else if r.Method == "GET" { + s, err := GetSession(r) + if err != nil { + WriteError(w, 1 /*Not Signed In*/) + return + } + + s.Write(w) + } else if r.Method == "DELETE" { + DeleteSessionIfExists(r) + WriteSuccess(w) + } +} diff --git a/transactions.go b/transactions.go new file mode 100644 index 0000000..8c68d05 --- /dev/null +++ b/transactions.go @@ -0,0 +1,33 @@ +package main + +import ( + "math/big" + "time" +) + +type Split struct { + SplitId int64 + TransactionId int64 + AccountId int64 + Number int64 // Check or reference number + Memo string + Amount big.Rat + Debit bool +} + +type TransactionStatus int64 + +const ( + Entered TransactionStatus = 1 + Cleared = 2 + Reconciled = 3 + Voided = 4 +) + +type Transaction struct { + TransactionId int64 + UserId int64 + Description string + Status TransactionStatus + Date time.Time +} diff --git a/users.go b/users.go new file mode 100644 index 0000000..d12c085 --- /dev/null +++ b/users.go @@ -0,0 +1,203 @@ +package main + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" +) + +type User struct { + UserId int64 + Name string + Username string + Password string `db:"-"` + PasswordHash string `json:"-"` + Email string +} + +const BogusPassword = "password" + +type UserExistsError struct{} + +func (ueu UserExistsError) Error() string { + return "User exists" +} + +func (u *User) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(u) +} + +func (u *User) Read(json_str string) error { + dec := json.NewDecoder(strings.NewReader(json_str)) + return dec.Decode(u) +} + +func (u *User) HashPassword() { + password_hasher := sha256.New() + io.WriteString(password_hasher, u.Password) + u.PasswordHash = fmt.Sprintf("%x", password_hasher.Sum(nil)) + u.Password = "" +} + +func GetUser(userid int64) (*User, error) { + var u User + + err := DB.SelectOne(&u, "SELECT * from users where UserId=?", userid) + if err != nil { + return nil, err + } + return &u, nil +} + +func GetUserByUsername(username string) (*User, error) { + var u User + + err := DB.SelectOne(&u, "SELECT * from users where Username=?", username) + if err != nil { + return nil, err + } + return &u, nil +} + +func InsertUser(u *User) error { + transaction, err := DB.Begin() + if err != nil { + return err + } + + existing, err := transaction.SelectInt("SELECT count(*) from users where Username=?", u.Username) + if err != nil { + transaction.Rollback() + return err + } + if existing > 0 { + transaction.Rollback() + return UserExistsError{} + } + + err = transaction.Insert(u) + if err != nil { + transaction.Rollback() + return err + } + + err = transaction.Commit() + if err != nil { + transaction.Rollback() + return err + } + + return nil +} + +func GetUserFromSession(r *http.Request) (*User, error) { + s, err := GetSession(r) + if err != nil { + return nil, err + } + return GetUser(s.UserId) +} + +func UserHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + user_json := r.PostFormValue("user") + if user_json == "" { + WriteError(w, 3 /*Invalid Request*/) + return + } + + var user User + err := user.Read(user_json) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + user.UserId = -1 + user.HashPassword() + + err = InsertUser(&user) + if err != nil { + if _, ok := err.(UserExistsError); ok { + WriteError(w, 4 /*User Exists*/) + } else { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + } + return + } + + WriteSuccess(w) + } else { + user, err := GetUserFromSession(r) + if err != nil { + WriteError(w, 1 /*Not Signed In*/) + return + } + + userid, err := GetURLID(r.URL.Path) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + + if userid != user.UserId { + WriteError(w, 2 /*Unauthorized Access*/) + return + } + + if r.Method == "GET" { + err = user.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } else if r.Method == "PUT" { + user_json := r.PostFormValue("user") + if user_json == "" { + WriteError(w, 3 /*Invalid Request*/) + return + } + + // Save old PWHash in case the new password is bogus + old_pwhash := user.PasswordHash + + err = user.Read(user_json) + if err != nil || user.UserId != userid { + WriteError(w, 3 /*Invalid Request*/) + return + } + + // If the user didn't create a new password, keep their old one + if user.Password != BogusPassword { + user.HashPassword() + } else { + user.Password = "" + user.PasswordHash = old_pwhash + } + + count, err := DB.Update(user) + if count != 1 || err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + + WriteSuccess(w) + } else if r.Method == "DELETE" { + count, err := DB.Delete(&user) + if count != 1 || err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + + WriteSuccess(w) + } + } +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..d25ec55 --- /dev/null +++ b/util.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + "strings" +) + +func GetURLID(url string) (int64, error) { + pieces := strings.Split(strings.Trim(url, "/"), "/") + return strconv.ParseInt(pieces[len(pieces)-1], 10, 0) +} + +func WriteSuccess(w http.ResponseWriter) { + fmt.Fprint(w, "{}") +}