Initial commit

This commit is contained in:
Aaron Lindsay 2015-06-25 22:36:58 -04:00
commit 0f393d2fbb
10 changed files with 557 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.swp

21
accounts.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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, "{}")
}