2018-12-25 19:22:13 +00:00
package scsusers
import (
"bytes"
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
"html/template"
"log"
2018-12-27 22:23:18 +00:00
"crypto/rand"
2018-12-28 19:33:36 +00:00
"encoding/json"
2018-12-27 22:32:14 +00:00
"encoding/base32"
2018-12-25 19:22:13 +00:00
"net/smtp"
"strings"
)
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
2019-01-02 19:04:40 +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 {
Username string ` json:"username" `
UserPerms map [ string ] string ` json:"perms" `
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
2019-01-02 19:04:40 +00:00
c . SMTPServer = smtpserver
2018-12-25 19:22:13 +00:00
SetRegistrationTemplate ( "" )
SetAlertTemplate ( "" )
SetRecoveryTemplate ( "" )
}
func UsernameAvailable ( username string ) bool {
2018-12-28 19:33:36 +00:00
if len ( username ) == 0 {
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 {
2019-01-02 15:21:05 +00:00
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 {
2019-01-02 15:21:05 +00:00
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 ) ) {
2019-01-02 15:21:05 +00:00
log . Printf ( "scsusers.Register: New user registration: %s from %s\n" , username , ip )
2018-12-27 22:09:23 +00:00
return true
}
2019-01-02 15:21:05 +00:00
log . Printf ( "scsusers.Register: Failed to send registration email, deleting user %s\n" , username )
2019-07-23 23:46:36 +00:00
q = fmt . Sprintf ( "delete from %s_auth where username ILIKE $1 AND password=$2" , c . TablePrefix )
2018-12-27 22:09:23 +00:00
_ , err = c . db . Exec ( q , username , string ( crypt ) )
if err != nil {
2019-01-02 15:21:05 +00:00
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 {
2019-07-23 23:46:36 +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
err := c . db . Get ( & crypt , q , username )
if err != nil {
2019-01-02 15:21:05 +00:00
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 {
2019-01-02 15:21:05 +00:00
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
Bump ( username )
return true
}
2019-01-02 15:21:05 +00:00
func ChangePassword ( username , oldpass , newpass string ) bool {
log . Println ( "scsusers.ChangePassword: Attempting password change for " + username )
2019-07-23 23:46:36 +00:00
q := fmt . Sprintf ( "select password from %s_auth where username ILIKE $1 AND status='active'" , c . TablePrefix )
2019-01-02 15:21:05 +00:00
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 )
2019-07-23 23:46:36 +00:00
q = fmt . Sprintf ( "update %s_auth set password=$2 where username ILIKE $1" , c . TablePrefix )
2019-01-02 15:21:05 +00:00
_ , 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
}
Bump ( username )
return true
}
2019-01-11 21:39:47 +00:00
func GetUserid ( username string ) int64 {
var i int64
2019-07-23 23:46:36 +00:00
q := fmt . Sprintf ( "select userid from %s_auth where username ILIKE $1" , c . TablePrefix )
2019-01-11 21:39:47 +00:00
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
}
2018-12-28 19:33:36 +00:00
func LoadUser ( username string ) ( UserData , error ) {
var u UserData
2019-07-23 23:46:36 +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
err := c . db . Get ( d , q , username )
if err != nil {
2019-01-02 15:21:05 +00:00
log . Printf ( "scsusers.LoadUser: Error loading user: %s : %s\n" , username , err . Error ( ) )
2018-12-28 19:33:36 +00:00
return u , err
}
err = json . Unmarshal ( [ ] byte ( d ) , & u )
if err != nil {
2019-01-02 15:21:05 +00:00
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
}
return u , err
}
func SaveUser ( username string , d UserData ) bool {
2019-07-23 23:46:36 +00:00
q := fmt . Sprintf ( "update %s_userdata set data=$1 where username ILIKE $2" )
2018-12-28 19:33:36 +00:00
j , err := json . Marshal ( d )
if err != nil {
2019-01-02 15:21:05 +00:00
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
}
_ , err = c . db . Exec ( q , username , j )
if err != nil {
2019-01-02 15:21:05 +00:00
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
2018-12-28 19:33:36 +00:00
2018-12-27 21:49:59 +00:00
}
2018-12-28 19:33:36 +00:00
func Bump ( username string ) {
2019-07-23 23:46:36 +00:00
q := fmt . Sprintf ( "update %s_auth set lastseen=CURRENT_TIMESTAMP where username ILIKE $1" , c . TablePrefix )
2018-12-27 21:49:59 +00:00
_ , err := c . db . Exec ( q , username )
if err != nil {
2019-01-02 15:21:05 +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 {
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
2019-07-23 23:46:36 +00:00
user_id = ( select userid from % s_auth where username ILIKE $ 1 ) ` ,
2019-07-23 23:34:54 +00:00
c . TablePrefix , c . TablePrefix )
rows , err := c . db . Queryx ( q , username )
if err != nil {
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
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 )
2019-07-23 23:34:54 +00:00
err := c . db . Get ( & v , q , username , metakey )
if err != nil {
log . Printf ( "scsusers.GetMeta: %s - %s - %s\n" , username , metakey , err . Error ( ) )
}
2019-07-25 22:23:20 +00:00
if v == "" {
// get default user
err := c . db . Get ( & v , q , "//default//" , metakey )
if err != nil {
log . Printf ( "scsusers.GetMeta: %s - %s - %s\n" , username , metakey , err . Error ( ) )
}
}
2019-07-23 23:34:54 +00:00
return v
}
func SetMeta ( username string , metakey string , metavalue string ) {
var err error
if metavalue == "" {
2019-07-23 23:46:36 +00:00
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 )
_ , err = c . db . Exec ( q , username , metakey )
} else {
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 )
2019-07-23 23:34:54 +00:00
_ , 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 ( ) )
}
}
2019-07-23 14:38:44 +00:00
2018-12-27 21:49:59 +00:00
func RecoverByUsername ( u string ) {
var username , email string
2019-07-23 23:46:36 +00:00
q := fmt . Sprintf ( "select username, email from %s_auth where username ILIKE $1" , c . TablePrefix )
2018-12-27 21:49:59 +00:00
row := c . db . QueryRow ( q , u )
err := row . Scan ( & username , & email )
if err != sql . ErrNoRows {
2018-12-28 19:33:36 +00:00
recoverycode := randBytes ( 16 )
2019-07-23 23:46:36 +00:00
qq := fmt . Sprintf ( "update %s_auth set recoverycode=$1, recoverytime=NOW() where username ILIKE $2" , c . TablePrefix )
2018-12-27 21:49:59 +00:00
_ , 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 {
2018-12-28 19:33:36 +00:00
recoverycode := randBytes ( 16 )
2019-07-23 23:46:36 +00:00
qq := fmt . Sprintf ( "update %s_auth set recoverycode=$1, recoverytime=NOW() where username ILIKE $2" , c . TablePrefix )
2018-12-27 21:49:59 +00:00
_ , err := c . db . Exec ( qq , recoverycode , username )
if err == nil {
sendRecoveryEmail ( email , username , string ( recoverycode ) )
}
}
}
2018-12-25 19:22:13 +00:00
func randBytes ( n int ) [ ] byte {
2018-12-27 22:32:14 +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 {
2019-01-02 15:21:05 +00:00
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 {
2019-01-02 15:21:05 +00:00
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
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 {
2019-01-02 15:21:05 +00:00
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
}
subject := fmt . Sprintf ( "New activity on %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 {
2019-01-02 15:21:05 +00:00
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 {
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 {
2019-01-02 15:21:05 +00:00
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
}
2019-01-02 15:21:05 +00:00
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 {
2019-01-02 15:21:05 +00:00
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 {
2019-01-02 15:21:05 +00:00
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 {
2019-01-02 15:21:05 +00:00
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
}
2019-07-23 14:38:44 +00:00
2018-12-25 19:22:13 +00:00
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 {
2019-01-02 15:21:05 +00:00
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 ( )
}