mirror of
				https://github.com/aclindsa/moneygo.git
				synced 2025-10-30 17:33:26 -04:00 
			
		
		
		
	Initial commit
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| *.swp | ||||
							
								
								
									
										21
									
								
								accounts.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								accounts.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										32
									
								
								db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								db.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										36
									
								
								errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								errors.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										77
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										19
									
								
								securities.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								securities.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										118
									
								
								sessions.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								sessions.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										33
									
								
								transactions.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								transactions.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										203
									
								
								users.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								users.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										17
									
								
								util.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								util.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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, "{}") | ||||
| } | ||||
		Reference in New Issue
	
	Block a user