commit 0f393d2fbb1cec213948fd2429b42a3715d4394e Author: Aaron Lindsay Date: Thu Jun 25 22:36:58 2015 -0400 Initial commit 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, "{}") +}