diff --git a/example/example.go b/example/example.go index decbb41..9bc3a0c 100644 --- a/example/example.go +++ b/example/example.go @@ -1,13 +1,14 @@ package main import ( - "gitto.work/shortcut/scsusers" - "github.com/jmoiron/sqlx" - "flag" - "os" - "fmt" -_ "github.com/mattn/go-sqlite3" - ) + "flag" + "fmt" + "os" + + "git.teamworkapps.com/shortcut/scsusers" + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" +) func main() { var email string @@ -27,14 +28,9 @@ func main() { os.Exit(1) } schema:=`CREATE TABLE test_auth ( - username text NOT NULL , - password text NOT NULL, - email text NOT NULL unique, - recovery text NOT NULL DEFAULT '', - recoverytime timestamp null, - registration_date timestamp not null, - registration_ip text not null, - lastseen timestamp );` + id int primary key autoincrement, + username text NOT NULL unique key + password text NOT NULL);` _ ,err=db.Exec(schema) if err != nil { fmt.Println("Schema creation failed: " + err.Error()) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3902c57 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.teamworkapps.com/shortcut/scsusers + +go 1.20 + +require ( + github.com/jmoiron/sqlx v1.3.5 + github.com/mattn/go-sqlite3 v1.14.17 + golang.org/x/crypto v0.12.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1c3ade1 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= diff --git a/main.go b/main.go index 30cd0d9..cb4e8f0 100644 --- a/main.go +++ b/main.go @@ -29,17 +29,27 @@ type config struct { Templates templates SMTPServer string db *sqlx.DB + testing bool TablePrefix string } type UserData struct { - Username string `json:"username"` - UserPerms map[string]string `json:"perms"` - UserSettings map[string]string `json:"settings"` + UserID int64 `db:"id"` + Username string `db:"username"` + Password string `db:"password"` + + Meta map[string]metadata +} + +type metadata struct { + Key string `db:"meta_key"` + Value string `db:"meta_value"` + ID int64 `db:"id"` } var c config + func Init(dbin *sqlx.DB, tp, sitename, fromaddr, smtpserver string) { c.db = dbin c.TablePrefix = tp @@ -85,18 +95,20 @@ func Register(username, email, ip string) bool { log.Printf("scsusers.Register: Bcrypt GenerateFromPassword failed? Pass is %s and error is %s\n", pass, err.Error()) return false } - q := fmt.Sprintf("insert into %s_auth (username, displayname, email, password, registration_date, registration_ip) values ($1, $2, $3, $4, CURRENT_TIMESTAMP, $5)", c.TablePrefix) - _, err = c.db.Exec(q, username, username, email, crypt, ip) + _, err = c.db.Query(fmt.Sprintf("insert into %s_auth (username, password) VALUES ($1, $2)", c.TablePrefix), username, crypt) if err != nil { log.Printf("scsusers.Register: insert failed: %s\n", err.Error()) return false } + if c.testing { + return true + } if SendRegistrationEmail(email, username, string(pass)) { log.Printf("scsusers.Register: New user registration: %s from %s\n", username, ip) return true } log.Printf("scsusers.Register: Failed to send registration email, deleting user %s\n", username) - q = fmt.Sprintf("delete from %s_auth where username = $1 AND password=$2", c.TablePrefix) + q := fmt.Sprintf("delete from %s_auth where username = $1 AND password=$2", c.TablePrefix) _, err = c.db.Exec(q, username, string(crypt)) if err != nil { log.Printf("scsusers.Register: Failed to delete new user %s: %s\n", username, err.Error()) @@ -104,17 +116,38 @@ func Register(username, email, ip string) bool { return false } -func Login(username, password string) bool { - username = strings.ToLower(username) +func NewUser() *UserData { + var u UserData + u.Meta = make(map[string]metadata) + return &u +} - q := fmt.Sprintf("select password from %s_auth where username = $1 AND status='active'", c.TablePrefix) - var crypt string - err := c.db.Get(&crypt, q, username) +func Get(username string) (*UserData, bool) { + + u := NewUser() + q := fmt.Sprintf("select username, password, id from %s_auth where username=$1", c.TablePrefix) + err := c.db.Get(&u, q, username) if err != nil { - log.Printf("scsusers.Login: Failed login attempt for unknown username: %s\n", username) + if err == sql.ErrNoRows { + return nil, false + } + log.Printf("scsusers.Get: %s", err.Error()) + return nil, false + } + q = fmt.Sprintf("select key, value, id from %s_meta where user=%d", c.TablePrefix, u.UserID) + err = c.db.Select(&u.Meta, q) + if err != nil && err != sql.ErrNoRows { + log.Printf("scsuser.Get: select: %s", err.Error()) + } + return u, true +} + +func Login(username, password string) bool { + u, ok := Get(username) + if !ok { return false } - if bcrypt.CompareHashAndPassword([]byte(crypt), []byte(password)) != nil { + if bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) != nil { log.Printf("scsusers.Login: Failed password for " + username) return false } @@ -122,25 +155,20 @@ func Login(username, password string) bool { return true } -func ChangePassword(username, oldpass, newpass string) bool { - username = strings.ToLower(username) - - q := fmt.Sprintf("select password from %s_auth where username = $1 AND status='active'", c.TablePrefix) - var crypt string - err := c.db.Get(&crypt, q, username) - if err != nil { - log.Println("scsusers.ChangePassword: Failed change attempt for unknown username: " + username) - return false - } - if bcrypt.CompareHashAndPassword([]byte(crypt), []byte(oldpass)) != nil { - log.Printf("scsusers.ChangePassword: Failed password for %s\n", username) +func (u *UserData) ChangePassword(oldpass, newpass string) bool { + if bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(oldpass)) != nil { + log.Printf("scsusers.ChangePassword: Failed password for %s\n", u.Username) return false } newcrypt, err := bcrypt.GenerateFromPassword([]byte(newpass), 10) - q = fmt.Sprintf("update %s_auth set password=$2 where username = $1", c.TablePrefix) - _, err = c.db.Exec(q, username, newcrypt) if err != nil { - log.Printf("scsusers.ChangePassword: update failed for %s: %s\n", username, err.Error()) + log.Printf("scsusers.ChangePassword: generate: %s", err.Error()) + return false + } + q := fmt.Sprintf("update %s_auth set password=$2 where userid=$1", c.TablePrefix) + _, err = c.db.Exec(q, u.UserID, newcrypt) + if err != nil { + log.Printf("scsusers.ChangePassword: update failed for %s: %s\n", u.Username, err.Error()) return false } return true @@ -159,22 +187,34 @@ func GetUserid(username string) int64 { } return i } -func LoadUser(username string) (UserData, error) { - var tmp UserData - username = strings.ToLower(username) - q := fmt.Sprintf("select data from %s_userdata where username = $1", c.TablePrefix) - var d string - err := c.db.Get(d, q, username) - if err != nil { - log.Printf("scsusers.LoadUser: Error loading user: %s : %s\n", username, err.Error()) - return tmp, err +func (u *UserData) Get(key string) (string, bool) { + tmp, ok := u.Meta[key] + return tmp.Value, ok +} + +func (u *UserData) Set(key, value string) error { + tmp, ok := u.Meta[key] + if ok { + _, err := c.db.Query(fmt.Sprintf("delete from %s_meta where id=$1", c.TablePrefix), tmp.ID) + if err != nil { + log.Printf("scsauth: set: delete: %s", err.Error()) + return err + } } - err = json.Unmarshal([]byte(d), &tmp) + var insertid int64 + err := c.db.Get(&insertid, fmt.Sprintf("insert into %s_meta (userid, meta_key, meta_value) VALUES ($1, $2, $3) returning id", c.TablePrefix), u.UserID, key, value) if err != nil { - log.Printf("scsusers.LoadUser: Error decoding json on user %s. Unmarshal returned %s\n", username, err.Error()) + log.Printf("scsauth: set: insert: %s", err.Error()) + return err } - return tmp, err + var m metadata + m.Key = key + m.Value = value + m.ID = insertid + u.Meta[key] = m + + return nil } func SaveUser(username string, d UserData) bool { @@ -191,10 +231,10 @@ func SaveUser(username string, d UserData) bool { return false } return true - } func Bump(username string, ip string) { + username = strings.ToLower(username) q := fmt.Sprintf("update %s_auth set lastseen=CURRENT_TIMESTAMP, lastseenip=$2 where username = $1 limit 1", c.TablePrefix) _, err := c.db.Exec(q, username, ip) diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..c83114c --- /dev/null +++ b/main_test.go @@ -0,0 +1,52 @@ +package scsusers + +import ( + "flag" + "fmt" + "os" + "testing" + + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" +) + +func TestUsers(t *testing.T) { + var email string + c.testing=true + flag.StringVar(&email, "email", "", "Email address to use for registration test") + flag.Parse() + + db, err := sqlx.Open("sqlite3", ":memory:") + if err != nil { + fmt.Println("Couldn't open sqlite3 in-memory db:" + err.Error()) + os.Exit(1) + } + + err = db.Ping() + if err != nil { + fmt.Println("Couldn't ping sqlite3 in-memory db:" + err.Error()) + os.Exit(1) + } + schema := `CREATE TABLE test_auth ( + id integer primary key autoincrement, + username text NOT NULL unique, + password text NOT NULL);` + _, err = db.Exec(schema) + if err != nil { + fmt.Println("Schema creation failed: " + err.Error()) + os.Exit(1) + } + + Init(db, "test", "Example Test", "nobody@nowhere.com", "localhost:25") + a := UsernameAvailable("testuser") + fmt.Printf("Initial test of username available: %v\n", a) + a = Register("testuser", email, "127.0.0.1") + fmt.Printf("Register returned %v\n", a) + fmt.Printf("Attempt to log in with invalid username returned %v\n", Login("baduser", "badpass")) + fmt.Printf("Enter code from email:") + var code string + fmt.Scan(&code) + a = Login("testuser", code) + fmt.Printf("Login returned %v\n", a) + +}