package scsusers import ( "bytes" "crypto/rand" "database/sql" "encoding/base32" "encoding/json" "fmt" "html/template" "log" "net/smtp" "strings" "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 TablePrefix string } type UserData struct { Username string `json:"username"` UserPerms map[string]string `json:"perms"` UserSettings map[string]string `json:"settings"` } 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 u string q := fmt.Sprintf("select username from %s_auth where username ILIKE $1", c.TablePrefix) err := c.db.Get(&u, 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 } 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 } q := fmt.Sprintf("insert into %s_auth (username, email, password, registration_date, registration_ip) values ($1, $2, $3, CURRENT_TIMESTAMP, $4)", c.TablePrefix) _, err = c.db.Exec(q, username, email, crypt, ip) if err != nil { log.Printf("scsusers.Register: insert failed: %s\n", err.Error()) return false } 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 ILIKE $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()) } return false } func Login(username, password string) bool { q := fmt.Sprintf("select password from %s_auth where username ILIKE $1 AND status='active'", c.TablePrefix) var crypt string err := c.db.Get(&crypt, q, username) if err != nil { log.Printf("scsusers.Login: Failed login attempt for unknown username: %s\n", username) return false } if bcrypt.CompareHashAndPassword([]byte(crypt), []byte(password)) != nil { log.Printf("scsusers.Login: Failed password for " + username) return false } log.Printf("User %s logged in\n", username) return true } func ChangePassword(username, oldpass, newpass string) bool { q := fmt.Sprintf("select password from %s_auth where username ILIKE $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) return false } newcrypt, err := bcrypt.GenerateFromPassword([]byte(newpass), 10) q = fmt.Sprintf("update %s_auth set password=$2 where username ILIKE $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()) return false } return true } func GetUserid(username string) int64 { var i int64 q := fmt.Sprintf("select userid from %s_auth where username ILIKE $1", 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 LoadUser(username string) (UserData, error) { var u UserData q := fmt.Sprintf("select data from %s_userdata where username ILIKE $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 u, err } err = json.Unmarshal([]byte(d), &u) if err != nil { log.Printf("scsusers.LoadUser: Error decoding json on user %s. Unmarshal returned %s\n", username, err.Error()) } return u, err } func SaveUser(username string, d UserData) bool { q := fmt.Sprintf("update %s_userdata set data=$1 where username ILIKE $2") 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 } func Bump(username string, ip string) { q := fmt.Sprintf("update %s_auth set lastseen=CURRENT_TIMESTAMP, set lastseenip=$2 where username ILIKE $1", c.TablePrefix) _, err := c.db.Exec(q, username, ip) if err != nil { log.Printf("scsusers.Bump: Error on user bump: %s : %s\n", username, err.Error()) } } 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) q := fmt.Sprintf(`select meta_key, meta_value from %s_user_metadata where user_id=(select userid from %s_auth where username ILIKE $1)`, 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\n", username, err.Error()) return meta } meta[m.MetaKey] = m.MetaValue } return meta } func GetMeta(username string, metakey string) string { var v string q := fmt.Sprintf(`select meta_value from %s_user_metadata where user_id=(select userid from %s_auth where username ILIKE $1) AND meta_key=$2`, 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 if metavalue == "" { q := fmt.Sprintf(`delete from %s_user_metadata where user_id=(select userid from %s_auth where username ILIKE $1) AND meta_key=$2`, 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 ILIKE $1), $2, $3)`, 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(u string) { var username, email string q := fmt.Sprintf("select username, email from %s_auth where username ILIKE $1", c.TablePrefix) row := c.db.QueryRow(q, u) err := row.Scan(&username, &email) if err != sql.ErrNoRows { recoverycode := randBytes(16) qq := fmt.Sprintf("update %s_auth set recoverycode=$1, recoverytime=NOW() where username ILIKE $2", 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, email from %s_auth where email=$1", c.TablePrefix) row := c.db.QueryRow(q, e) err := row.Scan(&username, &email) if err != sql.ErrNoRows { recoverycode := randBytes(16) qq := fmt.Sprintf("update %s_auth set recoverycode=$1, recoverytime=NOW() where username ILIKE $2", 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.Registration.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 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 you, enter the following code to regain access: {{.RecoveryCode}}
If this was not you, 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 } msg := "To: " + to + "\r\n" + "From: " + from + "\r\n" + "Subject: " + subject + "\r\n" + "Content-Type: text/html; charset=\"UTF-8\"\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() }