package scsusers import ( "bytes" "crypto/rand" "database/sql" "encoding/base32" "encoding/json" "fmt" "html/template" "log" "net/smtp" "strings" "time" "github.com/jmoiron/sqlx" "golang.org/x/crypto/bcrypt" ) type templates struct { Registration *template.Template Alert *template.Template Recovery *template.Template } type config struct { SiteName string FromEmail string Templates templates SMTPServer string db *sqlx.DB testing bool TablePrefix string } type UserData struct { 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 c.SiteName = sitename c.FromEmail = fromaddr c.SMTPServer = smtpserver SetRegistrationTemplate("") SetAlertTemplate("") SetRecoveryTemplate("") } func UsernameAvailable(username string) bool { if len(username) == 0 { return false } var tmp string username = strings.ToLower(username) q := fmt.Sprintf("select username from %s_auth where username = ?", c.TablePrefix) err := c.db.Get(&tmp, q, username) if err == sql.ErrNoRows { return true } if err != nil { log.Printf("UsernameAvailable returned error: " + err.Error() + " Query was " + q) return false } return false } /* Check for username availability, add to database, send email */ func Register(username, email, ip string) bool { if !UsernameAvailable(username) { return false } username = strings.ToLower(username) pass := randBytes(16) crypt, err := bcrypt.GenerateFromPassword(pass, 10) if err != nil { log.Printf("scsusers.Register: Bcrypt GenerateFromPassword failed? Pass is %s and error is %s\n", pass, err.Error()) return false } _, err = c.db.Query(fmt.Sprintf("insert into %s_auth (username, password) VALUES (?,?)", 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 = ? AND password=?", 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()) } return false } func NewUser() *UserData { var u UserData u.Meta = make(map[string]metadata) return &u } func Get(username string) (*UserData, bool) { u := NewUser() q := fmt.Sprintf("select username, password, id from %s_auth where username=?", c.TablePrefix) err := c.db.Get(u, q, username) if err != nil { if err == sql.ErrNoRows { return nil, false } log.Printf("scsusers.Get: %s", err.Error()) return nil, false } q = fmt.Sprintf("select meta_key, meta_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(u.Password), []byte(password)) != nil { log.Printf("scsusers.Login: Failed password for " + username) return false } log.Printf("User %s logged in\n", username) return true } 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) if err != nil { log.Printf("scsusers.ChangePassword: generate: %s", err.Error()) return false } q := fmt.Sprintf("update %s_auth set password=? where userid=?", c.TablePrefix) _, err = c.db.Exec(q, newcrypt, u.UserID) if err != nil { log.Printf("scsusers.ChangePassword: update failed for %s: %s\n", u.Username, err.Error()) return false } return true } func GetUserid(username string) int64 { var i int64 username = strings.ToLower(username) q := fmt.Sprintf("select userid from %s_auth where username = ?", c.TablePrefix) err := c.db.Get(&i, q, username) if err != nil { log.Printf("scsusers.getUserId: Error loading user: %s : %s\n", username, err.Error()) return 0 } return i } 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=?", c.TablePrefix), tmp.ID) if err != nil { log.Printf("scsauth: set: delete: %s", err.Error()) return err } } var insertid int64 err := c.db.Get(&insertid, fmt.Sprintf("insert into %s_meta (userid, meta_key, meta_value) VALUES (?,?,?) returning id", c.TablePrefix), u.UserID, key, value) if err != nil { log.Printf("scsauth: set: insert: %s", err.Error()) return 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 { username = strings.ToLower(username) q := fmt.Sprintf("update %s_userdata set data=? where username = ?", c.TablePrefix) j, err := json.Marshal(d) if err != nil { log.Printf("scsusers.SaveUser: json.Marshal failed for username %s : %s\n", username, err.Error()) return false } _, err = c.db.Exec(q, username, j) if err != nil { log.Printf("scsusers.SaveUser: db.Exec failed for username %s : %s\n", username, err.Error()) return false } return true } type Metadata struct { MetaKey string `db:"meta_key"` MetaValue string `db:"meta_value"` } func GetAllMeta(username string) map[string]string { meta := make(map[string]string) username = strings.ToLower(username) q := fmt.Sprintf(`select meta_key, meta_value from %s_user_metadata where user_id=(select userid from %s_auth where username = ?)`, c.TablePrefix, c.TablePrefix) rows, err := c.db.Queryx(q, username) if err != nil && err != sql.ErrNoRows { log.Printf("scsusers.GetAllMeta: %s: %s\n", username, err.Error()) return meta } var m Metadata for rows.Next() { err = rows.StructScan(&m) if err != nil { log.Printf("scsusers.GetAllMeta: StructScan: %s: %s\n", username, err.Error()) return meta } meta[m.MetaKey] = m.MetaValue } return meta } func GetMeta(username string, metakey string) string { var v string username = strings.ToLower(username) q := fmt.Sprintf(`select meta_value from %s_user_metadata where user_id=(select userid from %s_auth where username = ?) AND meta_key=?`, c.TablePrefix, c.TablePrefix) err := c.db.Get(&v, q, username, metakey) if err != nil && err != sql.ErrNoRows { log.Printf("scsusers.GetMeta: %s - %s - %s\n", username, metakey, err.Error()) } if v == "" { // get default user err := c.db.Get(&v, q, "//default//", metakey) if err != nil && err != sql.ErrNoRows { log.Printf("scsusers.GetMeta: %s - %s - %s\n", username, metakey, err.Error()) } } return v } func SetMeta(username string, metakey string, metavalue string) { var err error username = strings.ToLower(username) if metavalue == "" { q := fmt.Sprintf(`delete from %s_user_metadata where user_id=(select userid from %s_auth where username = ?) AND meta_key=?`, c.TablePrefix, c.TablePrefix) _, err = c.db.Exec(q, username, metakey) } else { q := fmt.Sprintf(`insert into %s_user_metadata (user_id, meta_key, meta_value) VALUES ((select userid from %s_auth where username = ?), ?, ?)`, c.TablePrefix, c.TablePrefix) _, err = c.db.Exec(q, username, metakey, metavalue) } if err != nil { log.Printf("scsusers.SetMeta: %s %s %s %s\n", username, metakey, metavalue, err.Error()) } } func RecoverByUsername(username string) { var email string username = strings.ToLower(username) q := fmt.Sprintf("select email from %s_auth where username = ?", c.TablePrefix) err := c.db.Get(&email, q, username) if err != sql.ErrNoRows { recoverycode := randBytes(16) qq := fmt.Sprintf("update %s_auth set recoverycode=?, recoverytime=NOW() where username = ?", c.TablePrefix) _, err := c.db.Exec(qq, recoverycode, username) if err == nil { SendRecoveryEmail(email, username, string(recoverycode)) } } } func RecoverByEmail(e string) { var username, email string q := fmt.Sprintf("select username from %s_auth where email ILIKE ?", c.TablePrefix) err := c.db.Get(&username, q, e) if err != sql.ErrNoRows { recoverycode := randBytes(16) qq := fmt.Sprintf("update %s_auth set recoverycode=?, recoverytime=NOW() where username = ?", c.TablePrefix) _, err := c.db.Exec(qq, recoverycode, username) if err == nil { SendRecoveryEmail(email, username, string(recoverycode)) } } } func randBytes(n int) []byte { randomBytes := make([]byte, 32) _, err := rand.Read(randomBytes) if err != nil { panic(err) } return []byte(base32.StdEncoding.EncodeToString(randomBytes)[:n]) } func SendRegistrationEmail(recipient, username, password string) bool { data := struct { SiteName string FromEmail string UserName string Pass string }{ SiteName: c.SiteName, FromEmail: c.FromEmail, UserName: username, Pass: password, } var body bytes.Buffer err := c.Templates.Registration.Execute(&body, data) if err != nil { log.Printf("scsusers.sendRegistrationEmail: Registration template failed to execute: %v returned %s\n", data, err.Error()) return false } subject := fmt.Sprintf("Welcome to %s", c.SiteName) err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient) if err != nil { log.Printf("scsusers.SendRegistrationEmail: Error sending mail to %s: %s\n", recipient, err.Error()) return false } return true } func SendAlertEmail(username, recipient, message string) bool { data := struct { SiteName string FromEmail string UserName string Activity string }{ SiteName: c.SiteName, FromEmail: c.FromEmail, UserName: username, Activity: message, } var body bytes.Buffer err := c.Templates.Alert.Execute(&body, data) if err != nil { log.Printf("scsusers.sendAlertEmail: Alert template failed to execute: %v returned %s\n", data, err.Error()) return false } subject := fmt.Sprintf("new Activity Notification on %s", c.SiteName) err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient) if err != nil { log.Printf("scsusers.sendAlertEmail: Error sending mail to %s: %s\n", recipient, err.Error()) return false } return true } func SendRecoveryEmail(recipient, username, code string) bool { data := struct { SiteName string FromEmail string UserName string RecoveryCode string }{ SiteName: c.SiteName, FromEmail: c.FromEmail, UserName: username, RecoveryCode: code, } var body bytes.Buffer err := c.Templates.Registration.Execute(&body, data) if err != nil { log.Printf("scsusers.sendRecoveryEmail: Recovery template failed to execute: %v returned %s\n", data, err.Error()) return false } subject := fmt.Sprintf("Account recovery at %s", c.SiteName) err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient) if err != nil { log.Printf("scsusers.sendRecoveryEmail: Error sending mail to %s: %s\n", recipient, err.Error()) return false } return true } func SetRegistrationTemplate(t string) bool { if len(t) != 0 { r, err := template.New("reg").Parse(t) if err != nil { c.Templates.Registration = r return true } } df := `

Hello {{.UserName}}! Welcome to {{.SiteName}}! We've created your account with the username you selected and the following password: {{.Pass}}
You can change your password to whatever you want once you log in.

` r, err := template.New("reg").Parse(df) if err != nil { log.Fatal("scsusers.SetRegistrationTemplate: Default registration template MUST compile. Error: " + err.Error()) } c.Templates.Registration = r return false } func SetAlertTemplate(t string) bool { if len(t) != 0 { r, err := template.New("alert").Parse(t) if err != nil { c.Templates.Alert = r return true } } df := `

Hey {{.UserName}}! Just letting you know that {{.Activity}}.
You can disable future notifications in your user settings.

` r, err := template.New("alert").Parse(df) if err != nil { log.Fatal("scsusers.SetAlertTemplate: Default alert template MUST compile. Error: " + err.Error()) } c.Templates.Alert = r return false } func SetRecoveryTemplate(t string) bool { if len(t) != 0 { r, err := template.New("recovery").Parse(t) if err != nil { c.Templates.Recovery = r return true } } df := `

Hello {{.UserName}}! Someone (hopefully you) has attempted an account recovery agt {{.SiteName}}. If this was yousername, enter the following code to regain access: {{.RecoveryCode}}
If this was not yousername, you can ignore this email.

` r, err := template.New("recovery").Parse(df) if err != nil { log.Fatal("scsusers.SetRecoveryTemplate: Default recovery template MUST compile. Error: " + err.Error()) } c.Templates.Recovery = r return false } func SendMail(addr, from, subject, body string, to string) error { r := strings.NewReplacer("\r\n", "", "\r", "", "\n", "", "%0a", "", "%0d", "") c, err := smtp.Dial(addr) if err != nil { return err } defer c.Close() if err = c.Mail(r.Replace(from)); err != nil { return err } to = r.Replace(to) if err = c.Rcpt(to); err != nil { return err } w, err := c.Data() if err != nil { return err } date := time.Now() msg := "To: " + to + "\r\n" + "From: " + from + "\r\n" + "Subject: " + subject + "\r\n" + "Content-Type: text/html; charset=\"UTF-8\"\r\n" + "Date: " + date.Format(time.RFC1123Z) + "\r\n" + "\r\n" + body _, err = w.Write([]byte(msg)) if err != nil { return err } err = w.Close() if err != nil { return err } return c.Quit() }