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"
"math/rand"
"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
db * sqlx . DB
TablePrefix string
}
var c config
func Init ( dbin * sqlx . DB , tp , sitename , fromaddr string ) {
c . db = dbin
c . TablePrefix = tp
c . SiteName = sitename
c . FromEmail = fromaddr
SetRegistrationTemplate ( "" )
SetAlertTemplate ( "" )
SetRecoveryTemplate ( "" )
}
func UsernameAvailable ( username string ) bool {
2018-12-27 22:09:23 +00:00
var u string
2018-12-25 19:22:13 +00:00
q := fmt . Sprintf ( "select username from %s_auth where username=$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
}
2018-12-27 22:09:23 +00:00
log . Println ( "getting random bytes" )
2018-12-25 19:22:13 +00:00
pass := randBytes ( 16 )
2018-12-27 22:09:23 +00:00
log . Println ( "Generating hash" )
crypt , err := bcrypt . GenerateFromPassword ( pass , 10 )
2018-12-25 19:22:13 +00:00
if err != nil {
log . Printf ( "Bcrypt GenerateFromPassword failed? Pass is %s and error is %s\n" , pass , err . Error ( ) )
return false
}
2018-12-27 22:09:23 +00:00
fmt . Println ( "db insert" )
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 {
2018-12-27 22:09:23 +00:00
log . Println ( "Register: insert failed: " + 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 ) ) {
return true
}
log . Println ( "Failed to send registration email, deleting user." )
q = fmt . Sprintf ( "delete from %s_auth where username=$1 AND password=$2" , c . TablePrefix )
_ , err = c . db . Exec ( q , username , string ( crypt ) )
if err != nil {
log . Println ( "Failed to delete new user " + username + " : " + err . Error ( ) )
}
return false
2018-12-25 19:22:13 +00:00
}
2018-12-27 21:49:59 +00:00
func Login ( username , password string ) bool {
q := fmt . Sprintf ( "select password from %s_auth where username=$1" , c . TablePrefix )
var crypt string
err := c . db . Get ( & crypt , q , username )
if err != nil {
log . Println ( "Failed login attempt for unknown username: " + username )
return false
}
2018-12-27 22:14:30 +00:00
if bcrypt . CompareHashAndPassword ( [ ] byte ( crypt ) , [ ] byte ( password ) ) != nil {
2018-12-27 21:49:59 +00:00
log . Println ( "Failed password for " + username )
return false
}
bump ( username )
return true
}
func bump ( username string ) {
2018-12-27 22:09:23 +00:00
q := fmt . Sprintf ( "update %s_auth set lastseen=CURRENT_TIMESTAMP where username=$1" , c . TablePrefix )
2018-12-27 21:49:59 +00:00
_ , err := c . db . Exec ( q , username )
if err != nil {
log . Println ( "Error on user bump: " + err . Error ( ) )
}
}
func RecoverByUsername ( u string ) {
var username , email string
q := fmt . Sprintf ( "select username, email from %s_auth where username=$1" , c . TablePrefix )
row := c . db . QueryRow ( q , u )
err := row . Scan ( & username , & email )
if err != sql . ErrNoRows {
recoverycode := randBytes ( 10 )
qq := fmt . Sprintf ( "update %s_auth set recoverycode=$1, recoverytime=NOW() where username=$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 ( 10 )
qq := fmt . Sprintf ( "update %s_auth set recoverycode=$1, recoverytime=NOW() where username=$2" , c . TablePrefix )
_ , 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 21:49:59 +00:00
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
2018-12-25 19:22:13 +00:00
b := make ( [ ] byte , n )
for i := range b {
b [ i ] = letterBytes [ rand . Intn ( len ( letterBytes ) ) ]
}
return b
}
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 ( "Registration template failed to execute: %v returned %s\n" , data , err . Error ( ) )
return false
}
subject := fmt . Sprintf ( "Welcome to %s" , c . SiteName )
err = SendMail ( "localhost:25" , c . FromEmail , subject , body . String ( ) , recipient )
if err != nil {
log . Printf ( "Error sending mail to %s: %s\n" , recipient , err . Error ( ) )
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 {
log . Printf ( "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 ( "localhost:25" , c . FromEmail , subject , body . String ( ) , recipient )
if err != nil {
log . Printf ( "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 ( "Registration template failed to execute: %v returned %s\n" , data , err . Error ( ) )
return false
}
subject := fmt . Sprintf ( "Welcome to %s" , c . SiteName )
err = SendMail ( "localhost:25" , c . FromEmail , subject , body . String ( ) , recipient )
if err != nil {
log . Printf ( "Error sending mail to %s: %s\n" , recipient , err . Error ( ) )
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 ( "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
}
}
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 ( "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 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 ( "Default alert 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 ( )
}