Use process to send emails

This commit is contained in:
Paul 2018-02-01 03:30:00 +01:00
parent 8b34c53550
commit c185c54aa6
9 changed files with 208 additions and 60 deletions

View file

@ -14,15 +14,6 @@
</span> </span>
</div> </div>
</div> </div>
<div class="field">
<label for="password" class="label is-hidden">Password</label>
<div class="control has-icons-left">
<input class="input is-medium is-shadowless" id="password" name="password" type="password" placeholder="Password" value=""/>
<span class="icon is-medium is-left">
<i class="fas fa-key"></i>
</span>
</div>
</div>
{{ .csrfField }} {{ .csrfField }}
<div class="field is-grouped-right"> <div class="field is-grouped-right">
<input class="button is-success is-fullwidth is-medium" type="submit" value="Sign Up"> <input class="button is-success is-fullwidth is-medium" type="submit" value="Sign Up">

View file

@ -1,7 +1,16 @@
package handlers package handlers
import ( import (
"bytes"
"fmt"
"net/http" "net/http"
"time"
"git.klink.asia/paul/certman/views"
"github.com/go-chi/chi"
"github.com/gorilla/securecookie"
"git.klink.asia/paul/certman/services" "git.klink.asia/paul/certman/services"
@ -12,17 +21,28 @@ func RegisterHandler(p *services.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) { return func(w http.ResponseWriter, req *http.Request) {
// Get parameters // Get parameters
email := req.Form.Get("email") email := req.Form.Get("email")
password := req.Form.Get("password")
user := models.User{} user := models.User{}
user.Email = email user.Email = email
user.SetPassword(password)
err := p.DB.Create(&user).Error // don't set a password, user will get password reset request via mail
user.HashedPassword = []byte{}
err := p.DB.CreateUser(&user)
if err != nil { if err != nil {
panic(err.Error) panic(err.Error)
} }
if err := createPasswordReset(p, &user); err != nil {
p.Sessions.Flash(w, req,
services.Flash{
Type: "danger",
Message: "The registration email could not be generated.",
},
)
http.Redirect(w, req, "/register", http.StatusFound)
}
p.Sessions.Flash(w, req, p.Sessions.Flash(w, req,
services.Flash{ services.Flash{
Type: "success", Type: "success",
@ -41,9 +61,7 @@ func LoginHandler(p *services.Provider) http.HandlerFunc {
email := req.Form.Get("email") email := req.Form.Get("email")
password := req.Form.Get("password") password := req.Form.Get("password")
user := models.User{} user, err := p.DB.GetUserByEmail(email)
err := p.DB.Where(&models.User{Email: email}).Find(&user).Error
if err != nil { if err != nil {
// could not find user // could not find user
p.Sessions.Flash( p.Sessions.Flash(
@ -82,3 +100,80 @@ func LoginHandler(p *services.Provider) http.HandlerFunc {
http.Redirect(w, req, "/certs", http.StatusSeeOther) http.Redirect(w, req, "/certs", http.StatusSeeOther)
} }
} }
func ConfirmEmailHandler(p *services.Provider) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
v := views.NewWithSession(req, p.Sessions)
switch req.Method {
case "GET":
token := chi.URLParam(req, "token")
pwr, err := p.DB.GetPasswordResetByToken(token)
_ = pwr
if err != nil {
v.RenderError(w, 404)
return
}
v.Render(w, "email-set-password")
case "POST":
password := req.Form.Get("password")
token := req.Form.Get("token")
pwr, err := p.DB.GetPasswordResetByToken(token)
if err != nil {
v.RenderError(w, 404)
return
}
user, err := p.DB.GetUserByID(pwr.UserID)
if err != nil {
v.RenderError(w, 500)
return
}
user.SetPassword(password)
//err := p.DB.UpdateUser(user.ID, &user)
if err != nil {
v.RenderError(w, 500)
return
}
err = p.DB.DeletePasswordResetsByUserID(pwr.UserID)
default:
v.RenderError(w, 405)
}
// try to get post params
fmt.Fprintln(w, "Okay.")
}
}
func createPasswordReset(p *services.Provider, user *models.User) error {
// create the reset request
pwr := models.PasswordReset{
UserID: user.ID,
Token: string(securecookie.GenerateRandomKey(32)),
ValidUntil: time.Now().Add(6 * time.Hour),
}
if err := p.DB.CreatePasswordReset(&pwr); err != nil {
return err
}
var subject string
var text *bytes.Buffer
if user.EmailValid {
subject = "Password reset"
text.WriteString("Somebody (hopefully you) has requested a password reset.\nClick below to reset your password:\n")
} else {
// If the user email has not been confirmed yet, send out
// "mail confirmation"-mail instead
subject = "Email confirmation"
text.WriteString("Hello, thanks you for signing up!\nClick below to verify this email address\n")
}
return p.Email.Send(user.Email, subject, text.String(), "")
}

View file

@ -31,8 +31,7 @@ func CreateCertHandler(p *services.Provider) http.HandlerFunc {
email := p.Sessions.GetUserEmail(req) email := p.Sessions.GetUserEmail(req)
certname := req.FormValue("certname") certname := req.FormValue("certname")
user := models.User{} user, err := p.DB.GetUserByEmail(email)
err := p.DB.Where(&models.User{Email: email}).Find(&user).Error
if err != nil { if err != nil {
fmt.Printf("Could not fetch user for mail %s\n", email) fmt.Printf("Could not fetch user for mail %s\n", email)
} }
@ -62,9 +61,10 @@ func CreateCertHandler(p *services.Provider) http.HandlerFunc {
} }
// Insert client into database // Insert client into database
if err := p.DB.Create(&client).Error; err != nil { _ = client
panic(err.Error()) //if err := p.DB.Create(&client).Error; err != nil {
} // panic(err.Error())
//}
p.Sessions.Flash(w, req, p.Sessions.Flash(w, req,
services.Flash{ services.Flash{

View file

@ -30,6 +30,7 @@ func main() {
Lifetime: 24 * time.Hour, Lifetime: 24 * time.Hour,
}, },
Email: &services.EmailConfig{ Email: &services.EmailConfig{
SMTPEnabled: false,
SMTPServer: "example.com", SMTPServer: "example.com",
SMTPPort: 25, SMTPPort: 25,
SMTPUsername: "test", SMTPUsername: "test",

39
models/model.go Normal file
View file

@ -0,0 +1,39 @@
package models
import (
"errors"
"time"
)
var (
// ErrNotImplemented gets thrown if some action was not attempted,
// because it is not implemented in the code yet.
ErrNotImplemented = errors.New("Not implemented")
)
// Model is a base model definition, including helpful fields for dealing with
// models in a database
type Model struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `sql:"index"`
}
// Client represent the OpenVPN client configuration
type Client struct {
Model
Name string
User User
UserID uint
Cert []byte
PrivateKey []byte
}
type ClientProvider interface {
CountClients() (uint, error)
CreateClient(*User) (*User, error)
ListClients(count, offset int) ([]*User, error)
GetClientByID(id uint) (*User, error)
DeleteClient(id uint) error
}

View file

@ -1,27 +1,11 @@
package models package models
import ( import (
"errors"
"time" "time"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
var (
// ErrNotImplemented gets thrown if some action was not attempted,
// because it is not implemented in the code yet.
ErrNotImplemented = errors.New("Not implemented")
)
// Model is a base model definition, including helpful fields for dealing with
// models in a database
type Model struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `sql:"index"`
}
// User represents a User of the system which is able to log in // User represents a User of the system which is able to log in
type User struct { type User struct {
Model Model
@ -50,27 +34,22 @@ func (u *User) CheckPassword(password string) error {
type UserProvider interface { type UserProvider interface {
CountUsers() (uint, error) CountUsers() (uint, error)
CreateUser(*User) (*User, error) CreateUser(*User) error
ListUsers(count, offset int) ([]*User, error) ListUsers(count, offset int) ([]*User, error)
GetUserByID(id uint) (*User, error) GetUserByID(id uint) (*User, error)
GetUserByEmail(email string) (*User, error) GetUserByEmail(email string) (*User, error)
DeleteUser(id uint) error DeleteUser(id uint) error
} }
// Client represent the OpenVPN client configuration type PasswordReset struct {
type Client struct {
Model Model
Name string User *User
User User
UserID uint UserID uint
Cert []byte Token string
PrivateKey []byte ValidUntil time.Time
} }
type ClientProvider interface { type PasswordResetProvider interface {
CountClients() (uint, error) CreatePasswordReset(*PasswordReset) error
CreateClient(*User) (*User, error) GetPasswordResetByToken(token string) (*PasswordReset, error)
ListClients(count, offset int) ([]*User, error)
GetClientByID(id uint) (*User, error)
DeleteClient(id uint) error
} }

View file

@ -63,7 +63,7 @@ func HandleRoutes(provider *services.Provider) http.Handler {
r.Post("/", handlers.LoginHandler(provider)) r.Post("/", handlers.LoginHandler(provider))
}) })
//r.Post("/confirm-email/{token}", handlers.ConfirmEmailHandler(db)) r.Post("/confirm-email/{token}", handlers.ConfirmEmailHandler(provider))
r.Route("/forgot-password", func(r chi.Router) { r.Route("/forgot-password", func(r chi.Router) {
r.Get("/", v("forgot-password")) r.Get("/", v("forgot-password"))

View file

@ -21,7 +21,7 @@ type DBConfig struct {
// DB is a wrapper around gorm.DB to provide custom methods // DB is a wrapper around gorm.DB to provide custom methods
type DB struct { type DB struct {
*gorm.DB gorm *gorm.DB
conf *DBConfig conf *DBConfig
} }
@ -38,39 +38,69 @@ func NewDB(conf *DBConfig) *DB {
db.LogMode(conf.Log) db.LogMode(conf.Log)
return &DB{ return &DB{
DB: db, gorm: db,
conf: conf, conf: conf,
} }
} }
// CountUsers returns the number of Users in the datastore // CountUsers returns the number of Users in the datastore
func (db *DB) CountUsers() (uint, error) { func (db *DB) CountUsers() (uint, error) {
return 0, ErrNotImplemented var count uint
err := db.gorm.Find(&models.User{}).Count(&count).Error
return count, err
} }
// CreateUser inserts a user into the datastore // CreateUser inserts a user into the datastore
func (db *DB) CreateUser(*models.User) (*models.User, error) { func (db *DB) CreateUser(user *models.User) error {
return nil, ErrNotImplemented err := db.gorm.Create(&user).Error
return err
} }
// ListUsers returns a slice of 'count' users, starting at 'offset' // ListUsers returns a slice of 'count' users, starting at 'offset'
func (db *DB) ListUsers(count, offset int) ([]*models.User, error) { func (db *DB) ListUsers(count, offset int) ([]*models.User, error) {
var users = make([]*models.User, 0) var users = make([]*models.User, 0)
return users, ErrNotImplemented err := db.gorm.Find(&users).Limit(count).Offset(offset).Error
return users, err
} }
// GetUserByID returns a single user by ID // GetUserByID returns a single user by ID
func (db *DB) GetUserByID(id uint) (*models.User, error) { func (db *DB) GetUserByID(id uint) (*models.User, error) {
return nil, ErrNotImplemented var user models.User
err := db.gorm.Where("id = ?", id).First(&user).Error
return &user, err
} }
// GetUserByEmail returns a single user by email // GetUserByEmail returns a single user by email
func (db *DB) GetUserByEmail(email string) (*models.User, error) { func (db *DB) GetUserByEmail(email string) (*models.User, error) {
return nil, ErrNotImplemented var user models.User
err := db.gorm.Where("email = ?", email).First(&user).Error
return &user, err
} }
// DeleteUser removes a user from the datastore // DeleteUser removes a user from the datastore
func (db *DB) DeleteUser(id uint) error { func (db *DB) DeleteUser(id uint) error {
return ErrNotImplemented var user models.User
err := db.gorm.Where("id = ?", id).Delete(&user).Error
return err
}
// CreatePasswordReset creates a new password reset token
func (db *DB) CreatePasswordReset(pwReset *models.PasswordReset) error {
err := db.gorm.Create(&pwReset).Error
return err
}
// GetPasswordResetByToken retrieves a PasswordReset by token
func (db *DB) GetPasswordResetByToken(token string) (*models.PasswordReset, error) {
var pwReset models.PasswordReset
err := db.gorm.Where("token = ?", token).First(&pwReset).Error
return &pwReset, err
}
// DeletePasswordResetsByUserID deletes all pending password resets for a user
func (db *DB) DeletePasswordResetsByUserID(uid uint) error {
err := db.gorm.Where("user_id = ?", uid).Delete(&models.PasswordReset{}).Error
return err
} }

View file

@ -14,6 +14,7 @@ var (
type EmailConfig struct { type EmailConfig struct {
From string From string
SMTPEnabled bool
SMTPServer string SMTPServer string
SMTPPort int SMTPPort int
SMTPUsername string SMTPUsername string
@ -45,12 +46,19 @@ func (email *Email) Send(to, subject, text, html string) error {
return ErrMailUninitializedConfig return ErrMailUninitializedConfig
} }
if !email.config.SMTPEnabled {
log.Printf("SMTP is disabled in config, printing out email text instead:\nTo: %s\n%s", to, text)
}
m := mail.NewMessage() m := mail.NewMessage()
m.SetHeader("From", email.config.From) m.SetHeader("From", email.config.From)
m.SetHeader("To", to) m.SetHeader("To", to)
m.SetHeader("Subject", subject) m.SetHeader("Subject", subject)
m.SetBody("text/plain", text) m.SetBody("text/plain", text)
m.AddAlternative("text/html", html)
if len(html) > 0 {
m.AddAlternative("text/html", html)
}
// put email in chan // put email in chan
email.mailChan <- m email.mailChan <- m
@ -65,6 +73,11 @@ func (email *Email) Daemon() {
return return
} }
if !email.config.SMTPEnabled {
log.Print("SMTP is disabled in config, emails will be printed instead.")
return
}
log.Print("Running mail sending routine") log.Print("Running mail sending routine")
d := mail.NewDialer( d := mail.NewDialer(