From 81d6988a50c7304834d42aa8f347d6efd88b493a Mon Sep 17 00:00:00 2001
From: HFO4 <912394456@qq.com>
Date: Fri, 21 Feb 2020 15:15:14 +0800
Subject: [PATCH] Feat: user register / send activate email

---
 models/migration.go         |  6 +--
 models/user.go              |  7 +--
 pkg/email/template.go       | 14 ++++++
 pkg/util/session.go         |  1 -
 routers/controllers/user.go | 11 +++++
 routers/router.go           |  2 +
 service/user/login.go       |  1 +
 service/user/register.go    | 93 +++++++++++++++++++++++++++++++++++++
 8 files changed, 126 insertions(+), 9 deletions(-)
 create mode 100644 service/user/register.go

diff --git a/models/migration.go b/models/migration.go
index 7c9fb2c..87fc173 100644
--- a/models/migration.go
+++ b/models/migration.go
@@ -83,8 +83,8 @@ func addDefaultSettings() {
 		{Name: "siteURL", Value: ``, Type: "basic"},
 		{Name: "siteName", Value: `Cloudreve`, Type: "basic"},
 		{Name: "siteStatus", Value: `open`, Type: "basic"},
-		{Name: "regStatus", Value: `0`, Type: "register"},
-		{Name: "defaultGroup", Value: `3`, Type: "register"},
+		{Name: "register_enabled", Value: `1`, Type: "register"},
+		{Name: "default_group", Value: `2`, Type: "register"},
 		{Name: "siteKeywords", Value: `网盘,网盘`, Type: "basic"},
 		{Name: "siteDes", Value: `Cloudreve`, Type: "basic"},
 		{Name: "siteTitle", Value: `平步云端`, Type: "basic"},
@@ -177,7 +177,7 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
 	}
 
 	for _, value := range defaultSettings {
-		DB.Where(Setting{Name: value.Name}).FirstOrCreate(&value)
+		DB.Where(Setting{Name: value.Name}).Create(&value)
 	}
 }
 
diff --git a/models/user.go b/models/user.go
index 66b8047..b56df9b 100644
--- a/models/user.go
+++ b/models/user.go
@@ -31,11 +31,9 @@ type User struct {
 	Password        string `json:"-"`
 	Status          int
 	GroupID         uint
-	ActivationKey   string `json:"-"`
 	Storage         uint64
 	OpenID          string `json:"-"`
 	TwoFactor       string `json:"-"`
-	Delay           int
 	Avatar          string
 	Options         string `json:"-",gorm:"type:text"`
 	Authn           string `gorm:"type:text"`
@@ -55,8 +53,8 @@ type User struct {
 // UserOption 用户个性化配置字段
 type UserOption struct {
 	ProfileOff      bool   `json:"profile_off,omitempty"`
-	PreferredPolicy uint   `json:"preferred_policy"`
-	PreferredTheme  string `json:"preferred_theme"`
+	PreferredPolicy uint   `json:"preferred_policy,omitempty"`
+	PreferredTheme  string `json:"preferred_theme,omitempty"`
 }
 
 // Root 获取用户的根目录
@@ -190,7 +188,6 @@ func GetUserByEmail(email string) (User, error) {
 func NewUser() User {
 	options := UserOption{}
 	return User{
-		Avatar:            "default",
 		OptionsSerialized: options,
 	}
 }
diff --git a/pkg/email/template.go b/pkg/email/template.go
index 809336c..ab327a7 100644
--- a/pkg/email/template.go
+++ b/pkg/email/template.go
@@ -19,3 +19,17 @@ func NewOveruseNotification(userName, reason string) (string, string) {
 	return fmt.Sprintf("【%s】空间容量超额提醒", options["siteName"]),
 		util.Replace(replace, options["over_used_template"])
 }
+
+// NewActivationEmail 新建激活邮件
+func NewActivationEmail(userName, activateURL string) (string, string) {
+	options := model.GetSettingByNames("siteName", "siteURL", "siteTitle", "mail_activation_template")
+	replace := map[string]string{
+		"{siteTitle}":     options["siteName"],
+		"{userName}":      userName,
+		"{activationUrl}": activateURL,
+		"{siteUrl}":       options["siteURL"],
+		"{siteSecTitle}":  options["siteTitle"],
+	}
+	return fmt.Sprintf("【%s】注册激活", options["siteName"]),
+		util.Replace(replace, options["mail_activation_template"])
+}
diff --git a/pkg/util/session.go b/pkg/util/session.go
index ffc00e9..705eee1 100644
--- a/pkg/util/session.go
+++ b/pkg/util/session.go
@@ -21,7 +21,6 @@ func SetSession(c *gin.Context, list map[string]interface{}) {
 // GetSession 获取session
 func GetSession(c *gin.Context, key string) interface{} {
 	s := sessions.Default(c)
-	Log().Debug("Key:%s Val:%s", key, s.Get(key))
 	return s.Get(key)
 }
 
diff --git a/routers/controllers/user.go b/routers/controllers/user.go
index 34a753b..83ecbba 100644
--- a/routers/controllers/user.go
+++ b/routers/controllers/user.go
@@ -129,6 +129,17 @@ func UserLogin(c *gin.Context) {
 	}
 }
 
+// UserRegister 用户注册
+func UserRegister(c *gin.Context) {
+	var service user.UserRegisterService
+	if err := c.ShouldBindJSON(&service); err == nil {
+		res := service.Register(c)
+		c.JSON(200, res)
+	} else {
+		c.JSON(200, ErrorResponse(err))
+	}
+}
+
 // User2FALogin 用户二步验证登录
 func User2FALogin(c *gin.Context) {
 	var service user.Enable2FA
diff --git a/routers/router.go b/routers/router.go
index 5349803..5e501fc 100644
--- a/routers/router.go
+++ b/routers/router.go
@@ -103,6 +103,8 @@ func InitMasterRouter() *gin.Engine {
 		{
 			// 用户登录
 			user.POST("session", controllers.UserLogin)
+			// 用户注册
+			user.POST("", middleware.IsFunctionEnabled("register_enabled"), controllers.UserRegister)
 			// 用户登录
 			user.POST("2fa", controllers.User2FALogin)
 			// 初始化QQ登录
diff --git a/service/user/login.go b/service/user/login.go
index 73c3791..42eaa9a 100644
--- a/service/user/login.go
+++ b/service/user/login.go
@@ -51,6 +51,7 @@ func (service *UserLoginService) Login(c *gin.Context) serializer.Response {
 	if model.IsTrueVal(isCaptchaRequired) {
 		// TODO 验证码校验
 		captchaID := util.GetSession(c, "captchaID")
+		util.DeleteSession(c, "captchaID")
 		if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) {
 			return serializer.ParamErr("验证码错误", nil)
 		}
diff --git a/service/user/register.go b/service/user/register.go
new file mode 100644
index 0000000..54d12e0
--- /dev/null
+++ b/service/user/register.go
@@ -0,0 +1,93 @@
+package user
+
+import (
+	model "github.com/HFO4/cloudreve/models"
+	"github.com/HFO4/cloudreve/pkg/auth"
+	"github.com/HFO4/cloudreve/pkg/email"
+	"github.com/HFO4/cloudreve/pkg/hashid"
+	"github.com/HFO4/cloudreve/pkg/serializer"
+	"github.com/HFO4/cloudreve/pkg/util"
+	"github.com/gin-gonic/gin"
+	"github.com/mojocn/base64Captcha"
+	"net/url"
+	"strings"
+)
+
+// UserRegisterService 管理用户注册的服务
+type UserRegisterService struct {
+	//TODO 细致调整验证规则
+	UserName    string `form:"userName" json:"userName" binding:"required,email"`
+	Password    string `form:"Password" json:"Password" binding:"required,min=4,max=64"`
+	CaptchaCode string `form:"captchaCode" json:"captchaCode"`
+}
+
+// Register 新用户注册
+func (service *UserRegisterService) Register(c *gin.Context) serializer.Response {
+	// 相关设定
+	options := model.GetSettingByNames("email_active", "reg_captcha")
+	// 检查验证码
+	isCaptchaRequired := model.IsTrueVal(options["reg_captcha"])
+	if isCaptchaRequired {
+		captchaID := util.GetSession(c, "captchaID")
+		util.DeleteSession(c, "captchaID")
+		if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) {
+			return serializer.ParamErr("验证码错误", nil)
+		}
+	}
+
+	// 相关设定
+	isEmailRequired := model.IsTrueVal(options["email_active"])
+	defaultGroup := model.GetIntSetting("default_group", 2)
+
+	// 创建新的用户对象
+	user := model.NewUser()
+	user.Email = service.UserName
+	user.Nick = strings.Split(service.UserName, "@")[0]
+	user.SetPassword(service.Password)
+	user.Status = model.Active
+	if isEmailRequired {
+		user.Status = model.NotActivicated
+	}
+	user.GroupID = uint(defaultGroup)
+
+	// 创建用户
+	if err := model.DB.Create(&user).Error; err != nil {
+		return serializer.DBErr("此邮箱已被使用", err)
+	}
+
+	// 发送激活邮件
+	if isEmailRequired {
+
+		// 签名激活请求API
+		base := model.GetSiteURL()
+		userID := hashid.HashID(user.ID, hashid.UserID)
+		controller, _ := url.Parse("/api/v3/user/activate/" + userID)
+		activateURL, err := auth.SignURI(auth.General, base.ResolveReference(controller).String(), 86400)
+		if err != nil {
+			return serializer.Err(serializer.CodeEncryptError, "无法签名激活URL", err)
+		}
+
+		// 取得签名
+		credential := activateURL.Query().Get("sign")
+
+		// 生成对用户访问的激活地址
+		controller, _ = url.Parse("/activate")
+		finalURL := base.ResolveReference(controller)
+		queries := finalURL.Query()
+		queries.Add("id", userID)
+		queries.Add("sign", credential)
+		finalURL.RawQuery = queries.Encode()
+
+		// 返送激活邮件
+		title, body := email.NewActivationEmail(user.Email,
+			strings.ReplaceAll(finalURL.String(), "/activate", "/#/activate"),
+		)
+		if err := email.Send(user.Email, title, body); err != nil {
+			return serializer.Err(serializer.CodeInternalSetting, "无法发送激活邮件", err)
+		}
+
+		return serializer.Response{Code: 203}
+	}
+
+	return serializer.Response{}
+}