scsusers/main.go

466 lines
14 KiB
Go
Raw Normal View History

2018-12-25 19:22:13 +00:00
package scsusers
import (
"bytes"
2020-06-04 20:02:46 +00:00
"crypto/rand"
2018-12-25 19:22:13 +00:00
"database/sql"
2020-06-04 20:02:46 +00:00
"encoding/base32"
"encoding/json"
2018-12-25 19:22:13 +00:00
"fmt"
"html/template"
"log"
"net/smtp"
"strings"
2020-06-04 20:02:46 +00:00
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
)
2018-12-27 21:49:59 +00:00
2018-12-25 19:22:13 +00:00
type templates struct {
Registration *template.Template
Alert *template.Template
Recovery *template.Template
}
type config struct {
SiteName string
FromEmail string
Templates templates
2020-06-04 20:02:46 +00:00
SMTPServer string
2018-12-25 19:22:13 +00:00
db *sqlx.DB
TablePrefix string
}
2018-12-28 19:33:36 +00:00
type UserData struct {
2020-06-04 20:02:46 +00:00
Username string `json:"username"`
UserPerms map[string]string `json:"perms"`
2018-12-28 19:33:36 +00:00
UserSettings map[string]string `json:"settings"`
}
2018-12-25 19:22:13 +00:00
var c config
2019-01-02 19:04:40 +00:00
func Init(dbin *sqlx.DB, tp, sitename, fromaddr, smtpserver string) {
2018-12-25 19:22:13 +00:00
c.db = dbin
c.TablePrefix = tp
c.SiteName = sitename
c.FromEmail = fromaddr
2020-06-04 20:02:46 +00:00
c.SMTPServer = smtpserver
2018-12-25 19:22:13 +00:00
SetRegistrationTemplate("")
SetAlertTemplate("")
SetRecoveryTemplate("")
}
func UsernameAvailable(username string) bool {
2020-06-04 20:02:46 +00:00
if len(username) == 0 {
2018-12-28 19:33:36 +00:00
return false
}
2018-12-27 22:09:23 +00:00
var u string
2019-07-23 23:46:36 +00:00
q := fmt.Sprintf("select username from %s_auth where username ILIKE $1", c.TablePrefix)
2018-12-27 22:09:23 +00:00
err := c.db.Get(&u, q, username)
2018-12-25 19:22:13 +00:00
if err == sql.ErrNoRows {
return true
}
2018-12-27 22:09:23 +00:00
if err != nil {
log.Printf("UsernameAvailable returned error: " + err.Error() + " Query was " + q)
return false
}
2018-12-25 19:22:13 +00:00
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)
2018-12-27 22:09:23 +00:00
crypt, err := bcrypt.GenerateFromPassword(pass, 10)
2018-12-25 19:22:13 +00:00
if err != nil {
log.Printf("scsusers.Register: Bcrypt GenerateFromPassword failed? Pass is %s and error is %s\n", pass, err.Error())
2018-12-25 19:22:13 +00:00
return false
}
2018-12-27 22:09:23 +00:00
q := fmt.Sprintf("insert into %s_auth (username, email, password, registration_date, registration_ip) values ($1, $2, $3, CURRENT_TIMESTAMP, $4)", c.TablePrefix)
2018-12-25 19:22:13 +00:00
_, err = c.db.Exec(q, username, email, crypt, ip)
if err != nil {
log.Printf("scsusers.Register: insert failed: %s\n", err.Error())
2018-12-25 19:22:13 +00:00
return false
}
2018-12-27 22:09:23 +00:00
if sendRegistrationEmail(email, username, string(pass)) {
log.Printf("scsusers.Register: New user registration: %s from %s\n", username, ip)
2018-12-27 22:09:23 +00:00
return true
}
log.Printf("scsusers.Register: Failed to send registration email, deleting user %s\n", username)
2020-06-04 20:02:46 +00:00
q = fmt.Sprintf("delete from %s_auth where username ILIKE $1 AND password=$2", c.TablePrefix)
_, err = c.db.Exec(q, username, string(crypt))
2018-12-27 22:09:23 +00:00
if err != nil {
log.Printf("scsusers.Register: Failed to delete new user %s: %s\n", username, err.Error())
2018-12-27 22:09:23 +00:00
}
return false
2018-12-25 19:22:13 +00:00
}
2018-12-27 21:49:59 +00:00
func Login(username, password string) bool {
2020-06-04 20:02:46 +00:00
q := fmt.Sprintf("select password from %s_auth where username ILIKE $1 AND status='active'", c.TablePrefix)
2018-12-27 21:49:59 +00:00
var crypt string
2020-06-04 20:02:46 +00:00
err := c.db.Get(&crypt, q, username)
2018-12-27 21:49:59 +00:00
if err != nil {
log.Printf("scsusers.Login: Failed login attempt for unknown username: %s\n", username)
2018-12-27 21:49:59 +00:00
return false
}
2018-12-27 22:14:30 +00:00
if bcrypt.CompareHashAndPassword([]byte(crypt), []byte(password)) != nil {
log.Printf("scsusers.Login: Failed password for " + username)
2018-12-27 21:49:59 +00:00
return false
}
2019-01-24 13:15:54 +00:00
log.Printf("User %s logged in\n", username)
2018-12-28 19:33:36 +00:00
return true
}
func ChangePassword(username, oldpass, newpass string) bool {
2020-06-04 20:02:46 +00:00
q := fmt.Sprintf("select password from %s_auth where username ILIKE $1 AND status='active'", c.TablePrefix)
var crypt string
2020-06-04 20:02:46 +00:00
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 {
2020-06-04 20:02:46 +00:00
log.Printf("scsusers.ChangePassword: Failed password for %s\n", username)
return false
}
newcrypt, err := bcrypt.GenerateFromPassword([]byte(newpass), 10)
2020-06-04 20:02:46 +00:00
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
2020-06-04 20:02:46 +00:00
}
2019-01-11 21:39:47 +00:00
func GetUserid(username string) int64 {
var i int64
2020-06-04 20:02:46 +00:00
q := fmt.Sprintf("select userid from %s_auth where username ILIKE $1", c.TablePrefix)
err := c.db.Get(&i, q, username)
2019-01-11 21:39:47 +00:00
if err != nil {
log.Printf("scsusers.getUserId: Error loading user: %s : %s\n", username, err.Error())
return 0
}
return i
}
2018-12-28 19:33:36 +00:00
func LoadUser(username string) (UserData, error) {
var u UserData
2020-06-04 20:02:46 +00:00
q := fmt.Sprintf("select data from %s_userdata where username ILIKE $1", c.TablePrefix)
2018-12-28 19:33:36 +00:00
var d string
2020-06-04 20:02:46 +00:00
err := c.db.Get(d, q, username)
2018-12-28 19:33:36 +00:00
if err != nil {
log.Printf("scsusers.LoadUser: Error loading user: %s : %s\n", username, err.Error())
2018-12-28 19:33:36 +00:00
return u, err
}
2020-06-04 20:02:46 +00:00
err = json.Unmarshal([]byte(d), &u)
2018-12-28 19:33:36 +00:00
if err != nil {
log.Printf("scsusers.LoadUser: Error decoding json on user %s. Unmarshal returned %s\n", username, err.Error())
2018-12-28 19:33:36 +00:00
}
2020-06-04 20:02:46 +00:00
return u, err
2018-12-28 19:33:36 +00:00
}
func SaveUser(username string, d UserData) bool {
2020-06-04 20:02:46 +00:00
q := fmt.Sprintf("update %s_userdata set data=$1 where username ILIKE $2")
j, err := json.Marshal(d)
2018-12-28 19:33:36 +00:00
if err != nil {
log.Printf("scsusers.SaveUser: json.Marshal failed for username %s : %s\n", username, err.Error())
2018-12-28 19:33:36 +00:00
return false
}
2020-06-04 20:02:46 +00:00
_, err = c.db.Exec(q, username, j)
2018-12-28 19:33:36 +00:00
if err != nil {
log.Printf("scsusers.SaveUser: db.Exec failed for username %s : %s\n", username, err.Error())
2018-12-28 19:33:36 +00:00
return false
}
2018-12-27 21:49:59 +00:00
return true
2020-06-04 20:02:46 +00:00
2018-12-27 21:49:59 +00:00
}
2020-06-04 20:02:46 +00:00
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)
2018-12-27 21:49:59 +00:00
if err != nil {
2020-06-04 20:02:46 +00:00
log.Printf("scsusers.Bump: Error on user bump: %s : %s\n", username, err.Error())
2018-12-27 21:49:59 +00:00
}
}
2019-07-23 14:38:44 +00:00
2019-07-23 23:34:54 +00:00
type Metadata struct {
2020-06-04 20:02:46 +00:00
MetaKey string `db:meta_key`
2019-07-23 23:34:54 +00:00
MetaValue string `db:meta_value`
}
2020-06-04 20:02:46 +00:00
func GetAllMeta(username string) map[string]string {
meta := make(map[string]string)
q := fmt.Sprintf(`select meta_key, meta_value
2019-07-23 23:34:54 +00:00
from %s_user_metadata where
2019-07-23 23:46:36 +00:00
user_id=(select userid from %s_auth where username ILIKE $1)`,
2020-06-04 20:02:46 +00:00
c.TablePrefix, c.TablePrefix)
rows, err := c.db.Queryx(q, username)
if err != nil && err != sql.ErrNoRows {
2019-07-23 23:34:54 +00:00
log.Printf("scsusers.GetAllMeta: %s: %s\n", username, err.Error())
return meta
}
2020-06-04 20:02:46 +00:00
var m Metadata
2019-07-23 23:34:54 +00:00
for rows.Next() {
2020-06-04 20:02:46 +00:00
err = rows.StructScan(&m)
2019-07-23 23:34:54 +00:00
if err != nil {
log.Printf("scsusers.GetAllMeta: StructScan: %s\n", username, err.Error())
return meta
}
2020-06-04 20:02:46 +00:00
meta[m.MetaKey] = m.MetaValue
2019-07-23 23:34:54 +00:00
}
return meta
}
func GetMeta(username string, metakey string) string {
var v string
2020-06-04 20:02:46 +00:00
q := fmt.Sprintf(`select meta_value from %s_user_metadata where
2019-07-23 23:46:36 +00:00
user_id=(select userid from %s_auth where username ILIKE $1) AND meta_key=$2`, c.TablePrefix, c.TablePrefix)
2020-06-04 20:02:46 +00:00
err := c.db.Get(&v, q, username, metakey)
if err != nil && err != sql.ErrNoRows {
2019-07-23 23:34:54 +00:00
log.Printf("scsusers.GetMeta: %s - %s - %s\n", username, metakey, err.Error())
}
2020-06-04 20:02:46 +00:00
if v == "" {
// get default user
err := c.db.Get(&v, q, "//default//", metakey)
if err != nil && err != sql.ErrNoRows {
2019-07-25 22:23:20 +00:00
log.Printf("scsusers.GetMeta: %s - %s - %s\n", username, metakey, err.Error())
}
2020-06-04 20:02:46 +00:00
2019-07-25 22:23:20 +00:00
}
2019-07-23 23:34:54 +00:00
return v
}
func SetMeta(username string, metakey string, metavalue string) {
var err error
2020-06-04 20:02:46 +00:00
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`,
2019-07-23 23:34:54 +00:00
c.TablePrefix, c.TablePrefix)
2020-06-04 20:02:46 +00:00
_, err = c.db.Exec(q, username, metakey)
2019-07-23 23:34:54 +00:00
} else {
2020-06-04 20:02:46 +00:00
q := fmt.Sprintf(`insert into %s_user_metadata (user_id, meta_key, meta_value) VALUES
2019-07-23 23:46:36 +00:00
((select userid from %s_auth where username ILIKE $1), $2, $3)`, c.TablePrefix, c.TablePrefix)
2020-06-04 20:02:46 +00:00
_, err = c.db.Exec(q, username, metakey, metavalue)
2019-07-23 23:34:54 +00:00
}
if err != nil {
log.Printf("scsusers.SetMeta: %s %s %s %s\n", username, metakey, metavalue, err.Error())
}
}
2018-12-27 21:49:59 +00:00
func RecoverByUsername(u string) {
var username, email string
2020-06-04 20:02:46 +00:00
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 {
2018-12-27 21:49:59 +00:00
sendRecoveryEmail(email, username, string(recoverycode))
}
}
}
func RecoverByEmail(e string) {
var username, email string
2020-06-04 20:02:46 +00:00
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 {
2018-12-27 21:49:59 +00:00
sendRecoveryEmail(email, username, string(recoverycode))
}
}
}
2018-12-25 19:22:13 +00:00
func randBytes(n int) []byte {
2020-06-04 20:02:46 +00:00
randomBytes := make([]byte, 32)
_, err := rand.Read(randomBytes)
if err != nil {
panic(err)
}
return []byte(base32.StdEncoding.EncodeToString(randomBytes)[:n])
2018-12-25 19:22:13 +00:00
}
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())
2018-12-25 19:22:13 +00:00
return false
}
subject := fmt.Sprintf("Welcome to %s", c.SiteName)
2019-01-02 19:04:40 +00:00
err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient)
2018-12-25 19:22:13 +00:00
if err != nil {
log.Printf("scsusers.SendRegistrationEmail: Error sending mail to %s: %s\n", recipient, err.Error())
2018-12-25 19:22:13 +00:00
return false
}
return true
}
2018-12-27 21:49:59 +00:00
func sendAlertEmail(username, recipient, message string) bool {
data := struct {
SiteName string
FromEmail string
UserName string
2020-06-04 20:02:46 +00:00
Activity string
2018-12-27 21:49:59 +00:00
}{
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())
2018-12-27 21:49:59 +00:00
return false
}
2020-06-04 20:02:46 +00:00
2018-12-27 21:49:59 +00:00
subject := fmt.Sprintf("New activity on %s", c.SiteName)
2020-06-04 20:02:46 +00:00
err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient)
2018-12-27 21:49:59 +00:00
if err != nil {
log.Printf("scsusers.sendAlertEmail: Error sending mail to %s: %s\n", recipient, err.Error())
2018-12-27 21:49:59 +00:00
return false
}
return true
}
func sendRecoveryEmail(recipient, username, code string) bool {
data := struct {
2020-06-04 20:02:46 +00:00
SiteName string
FromEmail string
UserName string
RecoveryCode string
2018-12-27 21:49:59 +00:00
}{
2020-06-04 20:02:46 +00:00
SiteName: c.SiteName,
FromEmail: c.FromEmail,
UserName: username,
RecoveryCode: code,
2018-12-27 21:49:59 +00:00
}
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())
2018-12-27 21:49:59 +00:00
return false
}
subject := fmt.Sprintf("Account recovery at %s", c.SiteName)
2019-01-02 19:04:40 +00:00
err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient)
2018-12-27 21:49:59 +00:00
if err != nil {
log.Printf("scsusers.sendRecoveryEmail: Error sending mail to %s: %s\n", recipient, err.Error())
2018-12-27 21:49:59 +00:00
return false
}
return true
}
2018-12-25 19:22:13 +00:00
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())
2018-12-25 19:22:13 +00:00
}
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
}
}
2018-12-27 21:49:59 +00:00
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>`
2018-12-25 19:22:13 +00:00
r, err := template.New("alert").Parse(df)
if err != nil {
log.Fatal("scsusers.SetAlertTemplate: Default alert template MUST compile. Error: " + err.Error())
2018-12-25 19:22:13 +00:00
}
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 you, enter the following code to regain access: {{.RecoveryCode}}<br> If this was not you, 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())
2018-12-25 19:22:13 +00:00
}
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()
}