Skip to content

Commit

Permalink
Email Verification (#135)
Browse files Browse the repository at this point in the history
Closes #125 
* Email Verification
* Little type fix
* Error fields fixed
  • Loading branch information
yusufpapurcu committed Jul 19, 2020
1 parent 49e6abb commit 6851c8d
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 22 deletions.
54 changes: 54 additions & 0 deletions internal/api/auth.go
Expand Up @@ -4,6 +4,9 @@ import (
"encoding/json"
"net/http"
"strings"
"time"

"github.com/gorilla/mux"

"github.com/dgrijalva/jwt-go"
"github.com/go-playground/validator/v10"
Expand All @@ -20,6 +23,7 @@ var (
NoToken = "Token could not found! "
TokenCreateErr = "Token could not be created"
SignupSuccess = "User created successfully"
VerifySuccess = "Email verified succesfully"
)

// Signup ...
Expand Down Expand Up @@ -61,6 +65,9 @@ func Signup(s storage.Store) http.HandlerFunc {
return
}

confirmationCode := app.RandomMD5Hash()
createdUser.ConfirmationCode = confirmationCode

// 5. Update user once to generate schema
updatedUser, err := app.GenerateSchema(s, createdUser)
if err != nil {
Expand All @@ -87,6 +94,11 @@ func Signup(s storage.Store) http.HandlerFunc {

go app.SendMail([]string{viper.GetString("email.admin")}, subject, body)

confirmationBody := "Last step for use Passwall\n\n"
confirmationBody += "Confirmation link: " + "http://" + viper.GetString("server.domain") + ":" + viper.GetString("server.port")
confirmationBody += "/auth/confirm/" + userDTO.Email + "/" + confirmationCode

go app.SendMail([]string{userDTO.Email}, "Passwall Email Confirmation", confirmationBody)
response := model.Response{
Code: http.StatusOK,
Status: Success,
Expand All @@ -96,6 +108,48 @@ func Signup(s storage.Store) http.HandlerFunc {
}
}

// Confirm ...
func Confirm(s storage.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
email := mux.Vars(r)["email"]
code := mux.Vars(r)["code"]
usr, err := s.Users().FindByEmail(email)
if err != nil {
errs := []string{"Email not found!", "Raw error: " + err.Error()}
message := "Email couldn't confirm!"
RespondWithErrors(w, http.StatusBadRequest, message, errs)
return
} else if !usr.EmailVerifiedAt.IsZero() {
errs := []string{"Email is already verified!"}
message := "Email couldn't confirm!"
RespondWithErrors(w, http.StatusBadRequest, message, errs)
return
} else if code != usr.ConfirmationCode {
errs := []string{"Confirmation code is wrong!"}
message := "Email couldn't confirm!"
RespondWithErrors(w, http.StatusBadRequest, message, errs)
return
}

updatedUser := model.ToUserDTO(usr)
updatedUser.EmailVerifiedAt = time.Now()

_, err = app.UpdateUser(s, usr, updatedUser, false)
if err != nil {
errs := []string{"User can't updated!", "Raw error: " + err.Error()}
message := "Email couldn't confirm!"
RespondWithErrors(w, http.StatusBadRequest, message, errs)
return
}
response := model.Response{
Code: http.StatusOK,
Status: VerifySuccess,
Message: SignupSuccess,
}
RespondWithJSON(w, http.StatusOK, response)
}
}

// Signin ...
func Signin(s storage.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
Expand Down
4 changes: 3 additions & 1 deletion internal/app/user.go
Expand Up @@ -48,7 +48,9 @@ func UpdateUser(s storage.Store, user *model.User, userDTO *model.UserDTO, isAut
user.Name = userDTO.Name
user.Email = userDTO.Email
user.MasterPassword = userDTO.MasterPassword

if !userDTO.EmailVerifiedAt.IsZero() {
user.EmailVerifiedAt = userDTO.EmailVerifiedAt
}
// This never changes
user.Schema = fmt.Sprintf("user%d", user.ID)

Expand Down
21 changes: 21 additions & 0 deletions internal/app/utils.go
@@ -0,0 +1,21 @@
package app

import (
"crypto/md5"
"crypto/rand"
"encoding/hex"
)

// GetMD5Hash ...
func GetMD5Hash(text []byte) string {
hasher := md5.New()
hasher.Write(text)
return hex.EncodeToString(hasher.Sum(nil))
}

// RandomMD5Hash returns random md5 hash for unique conifrim links
func RandomMD5Hash() string {
b := make([]byte, 16)
rand.Read(b)
return GetMD5Hash(b)
}
2 changes: 2 additions & 0 deletions internal/config/config.go
Expand Up @@ -109,6 +109,7 @@ func initializeConfig() {
}

func bindEnvs() {
viper.BindEnv("server.domain", "DOMAIN")
viper.BindEnv("server.port", "PORT")
viper.BindEnv("server.passphrase", "PW_SERVER_PASSPHRASE")
viper.BindEnv("server.secret", "PW_SERVER_SECRET")
Expand Down Expand Up @@ -141,6 +142,7 @@ func setDefaults() {

// Server defaults
viper.SetDefault("server.port", "3625")
viper.SetDefault("server.domain", "passwall.io")
viper.SetDefault("server.passphrase", "passphrase-for-encrypting-passwords-do-not-forget")
viper.SetDefault("server.secret", "secret-key-for-JWT-TOKEN")
viper.SetDefault("server.timeout", 24)
Expand Down
1 change: 1 addition & 0 deletions internal/router/router.go
Expand Up @@ -101,6 +101,7 @@ func (r *Router) initRoutes() {
// Auth endpoints
authRouter := mux.NewRouter().PathPrefix("/auth").Subrouter()
authRouter.HandleFunc("/signup", api.Signup(r.store)).Methods(http.MethodPost)
authRouter.HandleFunc("/confirm/{email}/{code}", api.Confirm(r.store)).Methods(http.MethodGet)
authRouter.HandleFunc("/signin", api.Signin(r.store)).Methods(http.MethodPost)
authRouter.HandleFunc("/refresh", api.RefreshToken(r.store)).Methods(http.MethodPost)
authRouter.HandleFunc("/check", api.CheckToken(r.store)).Methods(http.MethodPost)
Expand Down
46 changes: 25 additions & 21 deletions model/user.go
Expand Up @@ -8,30 +8,34 @@ import (

// User model should be something like this
type User struct {
ID uint `gorm:"primary_key" json:"id"`
UUID uuid.UUID `gorm:"type:uuid; type:varchar(100);"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at"`
Name string `json:"name"`
Email string `json:"email"`
MasterPassword string `json:"master_password"`
Secret string `json:"secret"`
Plan string `json:"plan"`
Schema string `json:"schema"`
Role string `json:"role"`
ID uint `gorm:"primary_key" json:"id"`
UUID uuid.UUID `gorm:"type:uuid; type:varchar(100);"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at"`
Name string `json:"name"`
Email string `json:"email"`
MasterPassword string `json:"master_password"`
Secret string `json:"secret"`
Plan string `json:"plan"`
Schema string `json:"schema"`
Role string `json:"role"`
ConfirmationCode string `json:"confirmation_code"`
EmailVerifiedAt time.Time `json:"email_verified_at"`
}

type UserDTO struct {
ID uint `json:"id"`
UUID uuid.UUID `json:"uuid"`
Name string `json:"name" validate:"max=100"`
Email string `json:"email" validate:"required,email"`
MasterPassword string `json:"master_password" validate:"required,max=100,min=6"`
Secret string `json:"secret"`
Plan string `json:"plan"`
Schema string `json:"schema"`
Role string `json:"role"`
ID uint `json:"id"`
UUID uuid.UUID `json:"uuid"`
Name string `json:"name" validate:"max=100"`
Email string `json:"email" validate:"required,email"`
MasterPassword string `json:"master_password" validate:"required,max=100,min=6"`
Secret string `json:"secret"`
Plan string `json:"plan"`
Schema string `json:"schema"`
Role string `json:"role"`
ConfirmationCode string `json:"confirmation_code"`
EmailVerifiedAt time.Time `json:"email_verified_at"`
}

type UserDTOTable struct {
Expand Down

0 comments on commit 6851c8d

Please sign in to comment.