525 lines
14 KiB
Go
525 lines
14 KiB
Go
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=?", c.TablePrefix)
|
|
rows,err:=c.db.Query(q,u.UserID )
|
|
if err != nil && err != sql.ErrNoRows {
|
|
log.Printf("scsuser.Get: select: %s", err.Error())
|
|
return u,false
|
|
}
|
|
|
|
for rows.Next() {
|
|
var m metadata
|
|
rows.Scan(m.Key,m.Value,m.ID)
|
|
u.Meta[m.Key]=m
|
|
}
|
|
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 := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html></head><body><p>Hello {{.UserName}}! Welcome to {{.SiteName}}! We've created your account with the username you selected and the following password: {{.Pass}}<br>You can change your password to whatever you want once you log in.</p></body></html>`
|
|
|
|
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 := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html></head><body><p> Hey {{.UserName}}! Just letting you know that {{.Activity}}.<br> You can disable future notifications in your user settings.</p></body></html>`
|
|
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 := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html></head><body><p>Hello {{.UserName}}! Someone (hopefully you) has attempted an account recovery agt {{.SiteName}}. If this was yousername, enter the following code to regain access: {{.RecoveryCode}}<br> If this was not yousername, you can ignore this email.</p></body></html>`
|
|
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()
|
|
}
|