diff --git a/2fa.go b/2fa.go new file mode 100644 index 0000000..f027edb --- /dev/null +++ b/2fa.go @@ -0,0 +1,44 @@ +package scsusers + +import ( + "crypto/rand" + "errors" + "fmt" + "math/big" + "time" +) + +func generate2fa() string { + num, err := rand.Int(rand.Reader, big.NewInt(999999)) + if err != nil { + return "918273" + } + return fmt.Sprintf("%06d", num) + +} +func Validate2FA(u *UserData, challenge string) bool { + v, ok := u.Get("2fa") + return ok && v == challenge +} + +func Send2FA(u *UserData) error { + code := generate2fa() + u.Set("2fa", code) + u.Set("2faexpires", fmt.Sprintf("%d", time.Now().Add(15*time.Minute).Unix())) + email,ok:=u.Get("email") + if !ok { + return errors.New("send2fa: no email") + } + firstname,ok:=u.Get("firstname") + if !ok { + return errors.New("send2fa: no firstname") + } + lastname,ok:=u.Get("lastname") + if !ok { + return errors.New("send2fa: no lastname") + } + fullname:=fmt.Sprintf("%s %s", firstname, lastname) + return Send2faEmail(email, fullname, code) + +} + diff --git a/emails.go b/emails.go new file mode 100644 index 0000000..c252b0d --- /dev/null +++ b/emails.go @@ -0,0 +1,196 @@ +package scsusers + +import ( + "bytes" + "crypto/rand" + "database/sql" + "encoding/base32" + "fmt" + "log" + "net/smtp" + "strings" + "time" +) + +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 = ?", 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 Send2faEmail(recipient, fullname, code string) error { + data := struct { + SiteName string + FromEmail string + FullName string + Code string + }{ + SiteName: c.SiteName, + FromEmail: c.FromEmail, + FullName: fullname, + Code: code, + } + var body bytes.Buffer + err := c.Templates.TwoFA.Execute(&body, data) + if err != nil { + return fmt.Errorf("scsusers.send2fayEmail: 2fa template failed to execute: %v returned %s", data, err.Error()) + } + subject := fmt.Sprintf("Two Factor Authentication Code at %s", c.SiteName) + err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient) + return err +} + +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() +} diff --git a/main.go b/main.go index eead13e..a6c685d 100644 --- a/main.go +++ b/main.go @@ -1,17 +1,12 @@ 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" @@ -21,6 +16,7 @@ type templates struct { Registration *template.Template Alert *template.Template Recovery *template.Template + TwoFA *template.Template } type config struct { @@ -59,6 +55,7 @@ func Init(dbin *sqlx.DB, tp, sitename, fromaddr, smtpserver string) { SetRegistrationTemplate("") SetAlertTemplate("") SetRecoveryTemplate("") + Set2faTemplate("") } func UsernameAvailable(username string) bool { @@ -134,16 +131,16 @@ func Get(username string) (*UserData, bool) { 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 ) + 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 + return u, false } for rows.Next() { var m metadata - rows.Scan(m.Key,m.Value,m.ID) - u.Meta[m.Key]=m + rows.Scan(m.Key, m.Value, m.ID) + u.Meta[m.Key] = m } return u, true } @@ -307,218 +304,3 @@ func SetMeta(username string, metakey string, metavalue string) { 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 := `

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 yousername, enter the following code to regain access: {{.RecoveryCode}}
If this was not yousername, 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 - } - - 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() -} diff --git a/templates.go b/templates.go new file mode 100644 index 0000000..53f506f --- /dev/null +++ b/templates.go @@ -0,0 +1,96 @@ +package scsusers + +import ( + "embed" + "html/template" + "log" +) + +//go:embed templates/*.html +var FS embed.FS + +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, err := FS.ReadFile("templates/registration.html") + if err != nil { + log.Fatal("Missing recovery template") + } + + r, err := template.New("reg").Parse(string(df)) + if err != nil { + log.Fatal("scsusers.SetRegistrationTemplate: Default registration template MUST compile. Error: " + err.Error()) + } + c.Templates.Registration = r + + return false +} + +func Set2faTemplate(t string) bool { + if len(t) != 0 { + r, err := template.New("2fa").Parse(t) + if err != nil { + c.Templates.Registration = r + return true + } + } + df, err := FS.ReadFile("templates/2fa.html") + if err != nil { + log.Fatal("Missing 2fa template") + } + + r, err := template.New("2fa").Parse(string(df)) + if err != nil { + log.Fatal("scsusers.Set2faTemplate: Default 2fa 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, err := FS.ReadFile("templates/alert.html") + if err != nil { + log.Fatal("Missing recovery template") + } + + r, err := template.New("alert").Parse(string(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, err := FS.ReadFile("templates/recovery.html") + if err != nil { + log.Fatal("Missing recovery template") + } + r, err := template.New("recovery").Parse(string(df)) + if err != nil { + log.Fatal("scsusers.SetRecoveryTemplate: Default recovery template MUST compile. Error: " + err.Error()) + } + c.Templates.Recovery = r + + return false +} diff --git a/templates/2fa.html b/templates/2fa.html new file mode 100644 index 0000000..a615d4d --- /dev/null +++ b/templates/2fa.html @@ -0,0 +1,16 @@ + + + + + + +

+ Hello {{.UserName}}! This is your Two Factor Authentication code to finish logging in to {{.SiteName}}. If you did not request this code, + please contact us immediately. This code will expire in 30 minutes.

+

Your 2FA code is: {{.Code}}

+

+ + + + diff --git a/templates/alert.html b/templates/alert.html new file mode 100644 index 0000000..4fb19dc --- /dev/null +++ b/templates/alert.html @@ -0,0 +1,11 @@ + + + + + +

Hey {{.UserName}}! Just letting you know that {{.Activity}}.
You can disable future notifications in your + user settings.

+ + + \ No newline at end of file diff --git a/templates/recovery.html b/templates/recovery.html new file mode 100644 index 0000000..aeb5dab --- /dev/null +++ b/templates/recovery.html @@ -0,0 +1,12 @@ + + + + + +

Hello {{.UserName}}! Someone (hopefully you) has attempted an account recovery at {{.SiteName}}. If this was + you, enter the following code to regain access: {{.RecoveryCode}}
If this was not you, you can safely + ignore this email.

+ + + \ No newline at end of file diff --git a/templates/registration.html b/templates/registration.html index 5d942f9..2d853d8 100644 --- a/templates/registration.html +++ b/templates/registration.html @@ -7,7 +7,7 @@

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. + You can change your password once you log in.