From 5ef953ff55441fc271f3686f3478a2c6f6ae84d1 Mon Sep 17 00:00:00 2001 From: akuma06 Date: Sat, 6 May 2017 21:21:39 +0200 Subject: [PATCH] User Package (WIP) Added services and utils needed --- config/env.go | 1 + router/userHandler.go | 47 ++++ service/user/cookieHelper.go | 157 ++++++++++++ service/user/formValidator.go | 69 ++++++ service/user/locale/en-us.all.json | 22 ++ service/user/permission/permission.go | 42 ++++ service/user/user.go | 341 ++++++++++++++++++++++++++ service/user/userHelper.go | 56 +++++ service/user/verification.go | 92 +++++++ util/crypto/crypto.go | 29 +++ util/timeHelper/timeHelper.go | 53 ++++ 11 files changed, 909 insertions(+) create mode 100644 router/userHandler.go create mode 100644 service/user/cookieHelper.go create mode 100644 service/user/formValidator.go create mode 100644 service/user/locale/en-us.all.json create mode 100644 service/user/permission/permission.go create mode 100644 service/user/user.go create mode 100644 service/user/userHelper.go create mode 100644 service/user/verification.go create mode 100644 util/crypto/crypto.go create mode 100644 util/timeHelper/timeHelper.go diff --git a/config/env.go b/config/env.go index 3999a39d..3d1c0504 100644 --- a/config/env.go +++ b/config/env.go @@ -5,4 +5,5 @@ const ( // DEVELOPMENT | TEST | PRODUCTION Environment = "DEVELOPMENT" // Environment = "PRODUCTION" + WebAddress = "nyaa.pantsu.cat" ) diff --git a/router/userHandler.go b/router/userHandler.go new file mode 100644 index 00000000..af4307f9 --- /dev/null +++ b/router/userHandler.go @@ -0,0 +1,47 @@ +package router + +import( + "github.com/gorilla/mux" + "net/http" + "html" + "strconv" + "github.com/ewhal/nyaa/model" + "github.com/ewhal/nyaa/service/user" +) + +// Getting View User Registration + +func UserRegisterFormHandler(w http.ResponseWriter, r *http.Request) { + +} + +// Getting View User Login +func UserLoginFormHandler(w http.ResponseWriter, r *http.Request) { + +} + +// Getting User Profile +func UserProfileHandler(w http.ResponseWriter, r *http.Request) { + +} + +// Getting View User Profile Update +func UserProfileFormHandler(w http.ResponseWriter, r *http.Request) { + +} + +// Post Registration controller +func UserRegisterPostHandler(w http.ResponseWriter, r *http.Request) { + +} + +// Post Login controller +func UserLoginPostHandler(w http.ResponseWriter, r *http.Request) { + +} + +// Post Profule Update controller +func UserProfilePostHandler(w http.ResponseWriter, r *http.Request) { + +} + diff --git a/service/user/cookieHelper.go b/service/user/cookieHelper.go new file mode 100644 index 00000000..5db7eaf1 --- /dev/null +++ b/service/user/cookieHelper.go @@ -0,0 +1,157 @@ +package userService + +import ( + "errors" + "net/http" + + "github.com/gorilla/securecookie" + "golang.org/x/crypto/bcrypt" + + "github.com/ewhal/nyaa/config" + "github.com/ewhal/nyaa/db" + "github.com/ewhal/nyaa/model" + "github.com/ewhal/nyaa/util/log" + "github.com/ewhal/nyaa/util/validation" + "github.com//modelHelper" +) + +var cookieHandler = securecookie.New( + securecookie.GenerateRandomKey(64), + securecookie.GenerateRandomKey(32)) + +// // UserName get username from a cookie. +// func UserName(c *gin.Context) (string, error) { +// var userName string +// request := c.Request +// cookie, err := request.Cookie("session") +// if err != nil { +// return userName, err +// } +// cookieValue := make(map[string]string) +// err = cookieHandler.Decode("session", cookie.Value, &cookieValue) +// if err != nil { +// return userName, err +// } +// userName = cookieValue["name"] +// return userName, nil +// } + +func Token(w http.ResponseWriter, r *http.Request) (string, error) { + var token string + request := c.Request + cookie, err := request.Cookie("session") + if err != nil { + return token, err + } + cookieValue := make(map[string]string) + err = cookieHandler.Decode("session", cookie.Value, &cookieValue) + if err != nil { + return token, err + } + token = cookieValue["token"] + if len(token) == 0 { + return token, errors.New("Token is empty.") + } + return token, nil +} + +// SetCookie sets a cookie. +func SetCookie(w http.ResponseWriter, token string) (int, error) { + value := map[string]string{ + "token": token, + } + encoded, err := cookieHandler.Encode("session", value) + if err != nil { + return http.StatusInternalServerError, err + } + cookie := &http.Cookie{ + Name: "session", + Value: encoded, + Path: "/", + } + http.SetCookie(w, cookie) + return http.StatusOK, nil +} + +// ClearCookie clears a cookie. +func ClearCookie(w http.ResponseWriter) (int, error) { + cookie := &http.Cookie{ + Name: "session", + Value: "", + Path: "/", + MaxAge: -1, + } + http.SetCookie(w, cookie) + return http.StatusOK, nil +} + +// SetCookieHandler sets a cookie with email and password. +func SetCookieHandler(w http.ResponseWriter, email string, pass string) (int, error) { + if email != "" && pass != "" { + log.Debugf("User email : %s , password : %s", email, pass) + var user model.User + isValidEmail := validation.EmailValidation(email) + if isValidEmail { + log.Debug("User entered valid email.") + if db.ORM.Where("email = ?", email).First(&user).RecordNotFound() { + return http.StatusNotFound, errors.New("User is not found.") + } + } else { + log.Debug("User entered username.") + if db.ORM.Where("username = ?", email).First(&user).RecordNotFound() { + return http.StatusNotFound, errors.New("User is not found.") + } + } + err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(pass)) + if err != nil { + return http.StatusUnauthorized, errors.New("Password incorrect.") + } + status, err := SetCookie(w, user.Token) + if err != nil { + return status, err + } + w.Header().Set("X-Auth-Token", user.Token) + return http.StatusOK, nil + } else { + return http.StatusNotFound, errors.New("User is not found.") + } +} + +// RegisterHanderFromForm sets cookie from a RegistrationForm. +func RegisterHanderFromForm(w http.ResponseWriter, registrationForm RegistrationForm) (int, error) { + email := registrationForm.Email + pass := registrationForm.Password + log.Debugf("RegisterHandler UserEmail : %s", email) + log.Debugf("RegisterHandler UserPassword : %s", pass) + return SetCookieHandler(w, email, pass) +} + +// RegisterHandler sets a cookie when user registered. +func RegisterHandler(w http.ResponseWriter, r *http.Request) (int, error) { + var registrationForm RegistrationForm + modelHelper.BindValueForm(®istrationForm, r) + return RegisterHanderFromForm(w, registrationForm) +} + +// CurrentUser get a current user. +func CurrentUser(r *http.Request) (model.User, error) { + var user model.User + var token string + var err error + token = r.Header.Get("X-Auth-Token") + if len(token) > 0 { + log.Debug("header token exist.") + } else { + token, err = Token(c) + log.Debug("header token not exist.") + if err != nil { + return user, err + } + } + if db.ORM.Select(config.UserPublicFields+", email").Where("token = ?", token).First(&user).RecordNotFound() { + return user, errors.New("User is not found.") + } + db.ORM.Model(&user).Association("Languages").Find(&user.Languages) + db.ORM.Model(&user).Association("Roles").Find(&user.Roles) + return user, nil +} diff --git a/service/user/formValidator.go b/service/user/formValidator.go new file mode 100644 index 00000000..1ca06130 --- /dev/null +++ b/service/user/formValidator.go @@ -0,0 +1,69 @@ +package userService + +import ( + "regexp" + "github.com/ewhal/nyaa/util/log" +) + +const EMAIL_REGEX = `(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})` + +func EmailValidation(email string) bool { + exp, err := regexp.Compile(EMAIL_REGEX) + if regexpCompiled := log.CheckError(err); regexpCompiled { + if exp.MatchString(email) { + return true + } + } + return false +} + +// RegistrationForm is used when creating a user. +type RegistrationForm struct { + Username string `form:"registrationUsername" binding:"required"` + Email string `form:"registrationEmail" binding:"required"` + Password string `form:"registrationPassword" binding:"required"` +} + +// RegistrationForm is used when creating a user authentication. +type LoginForm struct { + Email string `form:"loginEmail" binding:"required"` + Password string `form:"loginPassword" binding:"required"` +} + +// UserForm is used when updating a user. +type UserForm struct { + Email string `form:"email" binding:"required"` +} + +// PasswordForm is used when updating a user password. +type PasswordForm struct { + CurrentPassword string `form:"currentPassword" binding:"required"` + Password string `form:"newPassword" binding:"required"` +} + +// SendPasswordResetForm is used when sending a password reset token. +type SendPasswordResetForm struct { + Email string `form:"email" binding:"required"` +} + +// PasswordResetForm is used when reseting a password. +type PasswordResetForm struct { + PasswordResetToken string `form:"token" binding:"required"` + Password string `form:"newPassword" binding:"required"` +} + +// VerifyEmailForm is used when verifying an email. +type VerifyEmailForm struct { + ActivationToken string `form:"token" binding:"required"` +} + +// ActivateForm is used when activating user. +type ActivateForm struct { + Activation bool `form:"activation" binding:"required"` +} + +// UserRoleForm is used when adding or removing a role from a user. +type UserRoleForm struct { + UserId int `form:"userId" binding:"required"` + RoleId int `form:"roleId" binding:"required"` +} diff --git a/service/user/locale/en-us.all.json b/service/user/locale/en-us.all.json new file mode 100644 index 00000000..28eaf0b1 --- /dev/null +++ b/service/user/locale/en-us.all.json @@ -0,0 +1,22 @@ +[ + { + "id": "link", + "translation": "link" + }, + { + "id": "verify_email_title", + "translation": "Verify your email address for Goyangi." + }, + { + "id": "verify_email_content", + "translation": "Please click below link to verify your email." + }, + { + "id": "reset_password_title", + "translation": "Reset your password for goyangi." + }, + { + "id": "reset_password_content", + "translation": "Please click below link to reset your password." + } +] diff --git a/service/user/permission/permission.go b/service/user/permission/permission.go new file mode 100644 index 00000000..5542b2a1 --- /dev/null +++ b/service/user/permission/permission.go @@ -0,0 +1,42 @@ +package userPermission + +import ( + "errors" + "net/http" + + "github.com/ewhal/nyaa/model" + "github.com/ewhal/nyaa/service/user" + "github.com/ewhal/nyaa/util/log" + "github.com/gin-gonic/gin" +) + +// HasAdmin checks that user has an admin permission. +func HasAdmin(user *model.User) bool { + name := "admin" + for _, role := range user.Roles { + log.Debugf("HasAdmin role.Name : %s", role.Name) + if role.Name == name { + return true + } + } + return false +} + +// CurrentOrAdmin check that user has admin permission or user is the current user. +func CurrentOrAdmin(user *model.User, userId uint) bool { + log.Debugf("user.Id == userId %d %d %s", user.Id, userId, user.Id == userId) + return (HasAdmin(user) || user.Id == userId) +} + +// CurrentUserIdentical check that userId is same as current user's Id. +func CurrentUserIdentical(userId uint) (bool, error) { + currentUser, err := userService.CurrentUser(c) + if err != nil { + return false, errors.New("Auth failed.") + } + if currentUser.Id != userId { + return false, errors.New("User is not identical.") + } + + return true, nil +} \ No newline at end of file diff --git a/service/user/user.go b/service/user/user.go new file mode 100644 index 00000000..ff07e6cd --- /dev/null +++ b/service/user/user.go @@ -0,0 +1,341 @@ +package userService + +import ( + "errors" + "net/http" + "strconv" + + "golang.org/x/crypto/bcrypt" + + "github.com/ewhal/nyaa/config" + "github.com/ewhal/nyaa/db" + "github.com/ewhal/nyaa/model" + "github.com/ewhal/nyaa/util/crypto" + "github.com/ewhal/nyaa/util/log" + "github.com/ewhal/nyaa/util/modelHelper" + "github.com/ewhal/nyaa/util/timeHelper" +) + +var userFields []string = []string{"name", "email", "createdAt", "updatedAt"} + +// SuggestUsername suggest user's name if user's name already occupied. +func SuggestUsername(username string) string { + var count int + var usernameCandidate string + db.ORM.Model(model.User{}).Where(&model.User{Username: username}).Count(&count) + log.Debugf("count Before : %d", count) + if count == 0 { + return username + } else { + var postfix int + for { + usernameCandidate = username + strconv.Itoa(postfix) + log.Debugf("usernameCandidate: %s\n", usernameCandidate) + db.ORM.Model(model.User{}).Where(&model.User{Username: usernameCandidate}).Count(&count) + log.Debugf("count after : %d\n", count) + postfix = postfix + 1 + if count == 0 { + break + } + } + } + return usernameCandidate +} + +// CreateUserFromForm creates a user from a registration form. +func CreateUserFromForm(registrationForm RegistrationForm) (model.User, error) { + var user model.User + log.Debugf("registrationForm %+v\n", registrationForm) + modelHelper.AssignValue(&user, ®istrationForm) + user.Md5 = crypto.GenerateMD5Hash(user.Email) // Gravatar + token, err := crypto.GenerateRandomToken32() + if err != nil { + return user, errors.New("Token not generated.") + } + user.Token = token + user.TokenExpiration = timeHelper.FewDaysLater(config.AuthTokenExpirationDay) + log.Debugf("user %+v\n", user) + if db.ORM.Create(&user).Error != nil { + return user, errors.New("User is not created.") + } + return user, nil +} + +// CreateUser creates a user. +func CreateUser(w http.ResponseWriter, r *http.Request) (int, error) { + var user model.User + var registrationForm RegistrationForm + var status int + var err error + + modelHelper.BindValueForm(®istrationForm, r) + + password, err := bcrypt.GenerateFromPassword([]byte(registrationForm.Password), 10) + if err != nil { + return http.StatusInternalServerError, err + } + registrationForm.Password = string(password) + user, err = CreateUserFromForm(registrationForm) + if err != nil { + return http.StatusInternalServerError, err + } + SendVerificationToUser(user) + status, err = RegisterHandler(w) + return status, err +} + +// RetrieveUser retrieves a user. +func RetrieveUser(r *http.Request, id string) (*model.PublicUser, bool, uint, int, error) { + var user model.User + var currentUserId uint + var isAuthor bool + // var publicUser *model.PublicUser + // publicUser.User = &user + if db.ORM.Select(config.UserPublicFields).First(&user, id).RecordNotFound() { + return &model.PublicUser{User: &user}, isAuthor, currentUserId, http.StatusNotFound, errors.New("User is not found.") + } + currentUser, err := CurrentUser(r) + if err == nil { + currentUserId = currentUser.Id + isAuthor = currentUser.Id == user.Id + } + var currentPage int + + currentPage = 1 + + var likings []model.User + var likingCount int + db.ORM.Table("users_followers").Where("users_followers.user_id=?", user.Id).Count(&likingCount) + if err = db.ORM.Limit(config.LikingPerPage).Order(config.LikingOrder).Offset(offset).Select(config.UserPublicFields). + Joins("JOIN users_followers on users_followers.user_id=?", user.Id). + Where("users.id = users_followers.follower_id"). + Group("users.id").Find(&likings).Error; err != nil { + log.Fatal(err.Error()) + } + user.Likings = likings + var likingList model.LikingList + + var liked []model.User + var likedCount int + db.ORM.Table("users_followers").Where("users_followers.follower_id=?", user.Id).Count(&likedCount) + if err = db.ORM.Limit(config.LikedPerPage).Order(config.LikedOrder).Offset(offset).Select(config.UserPublicFields). + Joins("JOIN users_followers on users_followers.follower_id=?", user.Id). + Where("users.id = users_followers.user_id"). + Group("users.id").Find(&liked).Error; err != nil { + log.Fatal(err.Error()) + } + user.Liked = liked + + log.Debugf("user liking %v\n", user.Likings) + log.Debugf("user liked %v\n", user.Liked) + return &model.PublicUser{User: &user}, isAuthor, currentUserId, http.StatusOK, nil +} + +// RetrieveUsers retrieves users. +func RetrieveUsers() []*model.PublicUser { + var users []*model.User + var userArr []*model.PublicUser + db.ORM.Select(config.UserPublicFields).Find(&users) + for _, user := range users { + userArr = append(userArr, &model.PublicUser{User: user}) + } + return userArr +} + +// UpdateUserCore updates a user. (Applying the modifed data of user). +func UpdateUserCore(user *model.User) (int, error) { + user.Md5 = crypto.GenerateMD5Hash(user.Email) + token, err := crypto.GenerateRandomToken32() + if err != nil { + return http.StatusInternalServerError, errors.New("Token not generated.") + } + user.Token = token + user.TokenExpiration = timeHelper.FewDaysLater(config.AuthTokenExpirationDay) + if db.ORM.Save(user).Error != nil { + return http.StatusInternalServerError, errors.New("User is not updated.") + } + return http.StatusOK, nil +} + +// UpdateUser updates a user. +func UpdateUser(w http.ResponseWriter, r *http.Request, id string) (*model.User, int, error) { + var user model.User + if db.ORM.First(&user, id).RecordNotFound() { + return &user, http.StatusNotFound, errors.New("User is not found.") + } + switch r.FormValue("type") { + case "password": + var passwordForm PasswordForm + modelHelper.BindValueForm(&passwordForm, r) + log.Debugf("form %+v\n", passwordForm) + err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(passwordForm.CurrentPassword)) + if err != nil { + log.Error("Password Incorrect.") + return &user, http.StatusInternalServerError, errors.New("User is not updated. Password Incorrect.") + } else { + newPassword, err := bcrypt.GenerateFromPassword([]byte(passwordForm.Password), 10) + if err != nil { + return &user, http.StatusInternalServerError, errors.New("User is not updated. Password not Generated.") + } else { + passwordForm.Password = string(newPassword) + modelHelper.AssignValue(&user, &passwordForm) + } + } + default: + var form UserForm + modelHelper.BindValueForm(&form, binding.Form) + log.Debugf("form %+v\n", form) + modelHelper.AssignValue(&user, &form) + } + + status, err := UpdateUserCore(&user) + if err != nil { + return &user, status, err + } + status, err = SetCookie(w, user.Token) + return &user, status, err +} + +// DeleteUser deletes a user. +func DeleteUser(w http.ResponseWriter, id string) (int, error) { + var user model.User + if db.ORM.First(&user, id).RecordNotFound() { + return http.StatusNotFound, errors.New("User is not found.") + } + if db.ORM.Delete(&user).Error != nil { + return http.StatusInternalServerError, errors.New("User is not deleted.") + } + status, err := ClearCookie(w) + return status, err +} + +// AddRoleToUser adds a role to a user. +func AddRoleToUser(r *http.Request) (int, error) { + var form UserRoleForm + var user model.User + var role model.Role + var roles []model.Role + modelHelper.BindValueForm(&form, r) + + if db.ORM.First(&user, form.UserId).RecordNotFound() { + return http.StatusNotFound, errors.New("User is not found.") + } + if db.ORM.First(&role, form.RoleId).RecordNotFound() { + return http.StatusNotFound, errors.New("Role is not found.") + } + log.Debugf("user email : %s", user.Email) + log.Debugf("Role name : %s", role.Name) + db.ORM.Model(&user).Association("Roles").Append(role) + db.ORM.Model(&user).Association("Roles").Find(&roles) + if db.ORM.Save(&user).Error != nil { + return http.StatusInternalServerError, errors.New("Role not appended to user.") + } + return http.StatusOK, nil +} + +// RemoveRoleFromUser removes a role from a user. +func RemoveRoleFromUser(w http.ResponseWriter, r *http.Request, userId string, roleId string) (int, error) { + var user model.User + var role model.Role + if db.ORM.First(&user, userId).RecordNotFound() { + return http.StatusNotFound, errors.New("User is not found.") + } + if db.ORM.First(&role, roleId).RecordNotFound() { + return http.StatusNotFound, errors.New("Role is not found.") + } + + log.Debugf("user : %v\n", user) + log.Debugf("role : %v\n", role) + if db.ORM.Model(&user).Association("Roles").Delete(role).Error != nil { + return http.StatusInternalServerError, errors.New("Role is not deleted from user.") + } + return http.StatusOK, nil +} + +// RetrieveCurrentUser retrieves a current user. +func RetrieveCurrentUser(r *http.Request) (model.User, int, error) { + user, err := CurrentUser(r) + if err != nil { + return user, http.StatusInternalServerError, err + } + return user, http.StatusOK, nil +} + +// RetrieveUserByEmail retrieves a user by an email +func RetrieveUserByEmail(email string) (*model.PublicUser, string, int, error) { + var user model.User + if db.ORM.Unscoped().Select(config.UserPublicFields).Where("email like ?", "%"+email+"%").First(&user).RecordNotFound() { + return &model.PublicUser{User: &user}, email, http.StatusNotFound, errors.New("User is not found.") + } + return &model.PublicUser{User: &user}, email, http.StatusOK, nil +} + +// RetrieveUsersByEmail retrieves users by an email +func RetrieveUsersByEmail(email string) []*model.PublicUser { + var users []*model.User + var userArr []*model.PublicUser + db.ORM.Select(config.UserPublicFields).Where("email like ?", "%"+email+"%").Find(&users) + for _, user := range users { + userArr = append(userArr, &model.PublicUser{User: user}) + } + return userArr +} + +// RetrieveUserByUsername retrieves a user by username. +func RetrieveUserByUsername(username string) (*model.PublicUser, string, int, error) { + var user model.User + if db.ORM.Unscoped().Select(config.UserPublicFields).Where("username like ?", "%"+username+"%").First(&user).RecordNotFound() { + return &model.PublicUser{User: &user}, username, http.StatusNotFound, errors.New("User is not found.") + } + return &model.PublicUser{User: &user}, username, http.StatusOK, nil +} + +// RetrieveUserForAdmin retrieves a user for an administrator. +func RetrieveUserForAdmin(id string) (model.User, int, error) { + var user model.User + if db.ORM.First(&user, id).RecordNotFound() { + return user, http.StatusNotFound, errors.New("User is not found.") + } + db.ORM.Model(&user).Association("Languages").Find(&user.Languages) + db.ORM.Model(&user).Association("Roles").Find(&user.Roles) + return user, http.StatusOK, nil +} + +// RetrieveUsersForAdmin retrieves users for an administrator. +func RetrieveUsersForAdmin() []model.User { + var users []model.User + var userArr []model.User + db.ORM.Find(&users) + for _, user := range users { + db.ORM.Model(&user).Association("Languages").Find(&user.Languages) + db.ORM.Model(&user).Association("Roles").Find(&user.Roles) + userArr = append(userArr, user) + } + return userArr +} + +// ActivateUser toggle activation of a user. +func ActivateUser(r *http.Request, id string) (model.User, int, error) { + id := c.Params.ByName("id") + var user model.User + var form ActivateForm + modelHelper.BindValueForm(&form, r) + if db.ORM.First(&user, id).RecordNotFound() { + return user, http.StatusNotFound, errors.New("User is not found.") + } + user.Activation = form.Activation + if db.ORM.Save(&user).Error != nil { + return user, http.StatusInternalServerError, errors.New("User not activated.") + } + return user, http.StatusOK, nil +} + +// CreateUserAuthentication creates user authentication. +func CreateUserAuthentication(w http.ResponseWriter, r *http.Request) (int, error) { + var form LoginForm + modelHelper.BindValueForm(&form, r) + email := form.Email + pass := form.Password + status, err := SetCookieHandler(w, email, pass) + return status, err +} diff --git a/service/user/userHelper.go b/service/user/userHelper.go new file mode 100644 index 00000000..e1d48c31 --- /dev/null +++ b/service/user/userHelper.go @@ -0,0 +1,56 @@ +package userService + +import ( + "github.com/ewha/nyaa/db" + "github.com/ewha/nyaa/model" + // "github.com/gin-gonic/gin" + "errors" + "net/http" + + "github.com/ewha/nyaa/util/log" + "github.com/ewha/nyaa/util/retrieveHelper" + // "github.com/dorajistyle/goyangi/util/crypto" +) + +// FindUserByUserName creates a user. +func FindUserByUserName(appID int64, userName string) (model.User, int, error) { + var user model.User + var err error + // token := c.Request.Header.Get("X-Auth-Token") + if db.ORM.Where("app_id=? and name=?", appID, userName).First(&user).RecordNotFound() { + return user, http.StatusUnauthorized, err + } + return user, http.StatusOK, nil +} + +// FindOrCreateUser creates a user. +func FindOrCreateUser(appID int64, userName string) (model.User, int, error) { + var user model.User + var err error + + // if len(token) > 0 { + // log.Debug("header token exist.") + // } else { + // token, err = Token(c) + // log.Debug("header token not exist.") + // if err != nil { + // return user, http.StatusUnauthorized, err + // } + // } + log.Debugf("userName : %s\n", userName) + // log.Debugf("Error : %s\n", err.Error()) + if db.ORM.Where("app_id=? and name=?", appID, userName).First(&user).RecordNotFound() { + var user model.User + // return user, http.StatusBadRequest, err + user.Name = userName + // user.Token = token + user.AppID = appID + log.Debugf("user %+v\n", user) + if db.ORM.Create(&user).Error != nil { + return user, http.StatusBadRequest, errors.New("User is not created.") + } + log.Debugf("retrived User %v\n", user) + return user, http.StatusOK, nil + } + return user, http.StatusBadRequest, nil +} diff --git a/service/user/verification.go b/service/user/verification.go new file mode 100644 index 00000000..7d76717a --- /dev/null +++ b/service/user/verification.go @@ -0,0 +1,92 @@ +package userService + +import ( + "errors" + "net/http" + "time" + + "github.com/ewhal/nyaa/config" + "github.com/ewhal/nyaa/db" + "github.com/ewhal/nyaa/model" + "github.com/ewhal/nyaa/util/modelHandler" + "github.com/ewhal/nyaa/util/crypto" + "github.com/ewhal/nyaa/util/email" + "github.com/ewhal/nyaa/util/log" + "github.com/ewhal/nyaa/util/timeHelper" + + "github.com/nicksnyder/go-i18n/i18n" +) + +// SendEmailVerfication sends an email verification token via email. +func SendEmailVerfication(to string, token string, locale string) error { + T, _ := i18n.Tfunc(locale) + err := email.SendEmailFromAdmin(to, + T("verify_email_title"), + T("link")+" : "+config.WebAddress+"/verify/email/"+token, + T("verify_email_content")+"
"+config.WebAddress+"/verify/email/"+token+"") + return err +} + +// SendVerificationToUser sends an email verification token to user. +func SendVerificationToUser(user model.User) (int, error) { + var status int + var err error + user.ActivateUntil = timeHelper.TwentyFourHoursLater() + user.ActivationToken, err = crypto.GenerateRandomToken32() + if err != nil { + return http.StatusInternalServerError, err + } + user.Activation = false + log.Debugf("generated token : %s", user.ActivationToken) + status, err = UpdateUserCore(&user) + if err != nil { + return status, err + } + err = SendEmailVerfication(user.Email, user.ActivationToken, "en-us") + if err != nil { + return http.StatusInternalServerError, err + } + return http.StatusOK, err +} + +// SendVerification sends an email verification token. +func SendVerification(r *http.Request) (int, error) { + + var user model.User + currentUser, err := CurrentUser(r) + if err != nil { + return http.StatusUnauthorized, errors.New("Unauthorized.") + } + if db.ORM.First(&user, currentUser.Id).RecordNotFound() { + return http.StatusNotFound, errors.New("User is not found.") + } + status, err := SendVerificationToUser(user) + return status, err +} + +// EmailVerification verifies an email of user. +func EmailVerification(w http.ResponseWriter, r *http.Request) (int, error) { + var user model.User + var verifyEmailForm VerifyEmailForm + modelHandler.BindValueForm(&verifyEmailForm, r) + log.Debugf("verifyEmailForm.ActivationToken : %s", verifyEmailForm.ActivationToken) + if db.ORM.Where(&model.User{ActivationToken: verifyEmailForm.ActivationToken}).First(&user).RecordNotFound() { + return http.StatusNotFound, errors.New("User is not found.") + } + isExpired := timeHelper.IsExpired(user.ActivateUntil) + log.Debugf("passwordResetUntil : %s", user.ActivateUntil.UTC()) + log.Debugf("expired : %t", isExpired) + if isExpired { + return http.StatusForbidden, errors.New("token not valid.") + } + user.ActivationToken = "" + user.ActivateUntil = time.Now() + user.ActivatedAt = time.Now() + user.Activation = true + status, err := UpdateUserCore(&user) + if err != nil { + return status, err + } + status, err = SetCookie(<, user.Token) + return status, err +} diff --git a/util/crypto/crypto.go b/util/crypto/crypto.go new file mode 100644 index 00000000..cbb912a3 --- /dev/null +++ b/util/crypto/crypto.go @@ -0,0 +1,29 @@ +package crypto + +import ( + "crypto/md5" + "fmt" + "strings" +) + +func GenerateMD5Hash(email string) string { + email = strings.ToLower(strings.TrimSpace(email)) + hash := md5.New() + hash.Write([]byte(email)) + return fmt.Sprintf("%x", hash.Sum(nil)) +} +func GenerateRandomToken16() (string, error) { + return GenerateRandomToken(16) +} + +func GenerateRandomToken32() (string, error) { + return GenerateRandomToken(32) +} + +func GenerateRandomToken(n int) (string, error) { + token := make([]byte, n) + _, err := rand.Read(token) + // %x base 16, lower-case, two characters per byte + return fmt.Sprintf("%x", token), err + +} diff --git a/util/timeHelper/timeHelper.go b/util/timeHelper/timeHelper.go new file mode 100644 index 00000000..d7634c8b --- /dev/null +++ b/util/timeHelper/timeHelper.go @@ -0,0 +1,53 @@ +package timeHelper + +import ( + "time" + + "github.com/ewhal/nyaa/util/log" +) + +func FewDaysLater(day int) time.Time { + return FewDurationLater(time.Duration(day) * 24 * time.Hour) +} + +func TwentyFourHoursLater() time.Time { + return FewDurationLater(time.Duration(24) * time.Hour) +} + +func SixHoursLater() time.Time { + return FewDurationLater(time.Duration(6) * time.Hour) +} + +func InTimeSpan(start, end, check time.Time) bool { + log.Debugf("check after before: %s %t %t\n", check, check.After(start), check.Before(end)) + return check.After(start) && check.Before(end) +} + +func InTimeSpanNow(start, end time.Time) bool { + now := time.Now() + return InTimeSpan(start, end, now) +} + +func FewDurationLater(duration time.Duration) time.Time { + // When Save time should considering UTC + // baseTime := time.Now() + // log.Debugf("basetime : %s", baseTime) + fewDurationLater := time.Now().Add(duration) + log.Debugf("time : %s", fewDurationLater) + return fewDurationLater +} + +func FewDurationLaterMillisecond(duration time.Duration) int64 { + return FewDurationLater(duration).UnixNano() / int64(time.Millisecond) +} + +func IsExpired(expirationTime time.Time) bool { + // baseTime := time.Now() + // log.Debugf("basetime : %s", baseTime) + log.Debugf("expirationTime : %s", expirationTime) + // elapsed := time.Since(expirationTime) + // log.Debugf("elapsed : %s", elapsed) + after := time.Now().After(expirationTime) + log.Debugf("after : %t", after) + return after +}