diff --git a/config.example.yml b/config.example.yml
index 4d19ee3..8540367 100644
--- a/config.example.yml
+++ b/config.example.yml
@@ -10,6 +10,13 @@ database:
redis:
host: ms-redis
port: 6379
+smtp:
+ host:
+ port:
+ user:
+ password:
+ ssl: false
system:
+ base_url:
verify: false
mount_dir: /path/to/mount
diff --git a/frontend/src/api.js b/frontend/src/api.js
index 74c9aab..89d9c96 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -35,6 +35,12 @@ export default {
UserRegister(data) {
return ajax('user/register', 'post', { data })
},
+ UserVerifyRequest() {
+ return ajax(`user/verify`, 'post', {})
+ },
+ UserVerify(code) {
+ return ajax(`user/verify/${code}`, 'get', {})
+ },
UserInstancesList(params) {
return ajax('user/instances', 'get', { params })
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index 7e7b625..3bf0b9a 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -3,6 +3,7 @@ import { createRouter, createWebHistory } from 'vue-router';
import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue';
+import Verify from '@/views/Verify.vue';
import InstanceCreate from '@/views/users/InstanceCreate.vue';
import InstanceList from '@/views/users/InstanceList.vue';
@@ -32,6 +33,11 @@ const router = createRouter({
name: 'register',
component: Register
},
+ {
+ path: '/verify/:code',
+ name: 'verify',
+ component: Verify
+ },
{
path: '/',
name: 'dashboard',
diff --git a/frontend/src/views/Verify.vue b/frontend/src/views/Verify.vue
new file mode 100644
index 0000000..266cea9
--- /dev/null
+++ b/frontend/src/views/Verify.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
邮箱验证
+
验证邮箱地址
+
+
+
+
+
+
+
+ 验证中
+
+
+ 请稍后
+
+
+
+
+
+
+
+
+
+
+ 验证成功
+
+
+ 你可以开始使用了
+
+
+
+
+
+
+
+
+
+
+ 验证失败
+
+
+ 请重新验证
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/users/Settings.vue b/frontend/src/views/users/Settings.vue
index 2ac9230..95d98db 100644
--- a/frontend/src/views/users/Settings.vue
+++ b/frontend/src/views/users/Settings.vue
@@ -1,3 +1,105 @@
- 个人设置
-
\ No newline at end of file
+
+
+
+
+
+
邮箱验证
+
+ 邮箱
+
+
+
+
+
+
+
+
+
+
+
修改邮箱
+
+ 邮箱
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
修改密码
+
+ 原密码
+
+
+
+ 新密码
+
+
+
+ 重复密码
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/models/users.go b/models/users.go
index 9ec3162..bba634f 100644
--- a/models/users.go
+++ b/models/users.go
@@ -16,7 +16,8 @@ type Users struct {
Password string `json:"password,omitempty" gorm:"type:varchar(255);not null"`
Role int `json:"role" gorm:"not null,default:0"`
- Email string `json:"email" gorm:"type:varchar(255);uniqueIndex;unique;not null"`
+ Email string `json:"email" gorm:"type:varchar(255);uniqueIndex;unique;not null"`
+ Verify bool `json:"verify" gorm:"not null,default:false"`
Balance float64 `json:"balance" gorm:"not null"`
diff --git a/routers/api/v1/admin/users/detail.go b/routers/api/v1/admin/users/detail.go
index adc1392..ed086d3 100644
--- a/routers/api/v1/admin/users/detail.go
+++ b/routers/api/v1/admin/users/detail.go
@@ -20,7 +20,7 @@ func detailHandler(ctx iris.Context) {
user := models.Users{
ID: id,
}
- result := database.DB.Select("id", "username", "email", "role", "balance", "created_at").First(&user)
+ result := database.DB.Select("id", "username", "email", "role", "verify", "balance", "created_at").First(&user)
if result.Error != nil {
l.Error("detail user error: %v", result.Error)
middleware.Error(ctx, middleware.CodeAdminUserDetailError, iris.StatusInternalServerError)
diff --git a/routers/api/v1/admin/users/list.go b/routers/api/v1/admin/users/list.go
index e8a9ea2..c73841b 100644
--- a/routers/api/v1/admin/users/list.go
+++ b/routers/api/v1/admin/users/list.go
@@ -42,7 +42,7 @@ func listHandler(ctx iris.Context) {
return
}
- result := database.DB.Limit(limit).Offset(offset).Select("id", "username", "email", "role", "balance", "created_at").Order("id").Find(&users)
+ result := database.DB.Limit(limit).Offset(offset).Select("id", "username", "email", "role", "verify", "balance", "created_at").Order("id").Find(&users)
if result.Error != nil {
l.Error("list users error: %v", result.Error)
middleware.Error(ctx, middleware.CodeAdminUserListError, iris.StatusInternalServerError)
diff --git a/routers/api/v1/admin/users/modify.go b/routers/api/v1/admin/users/modify.go
index 32aa034..8e4c955 100644
--- a/routers/api/v1/admin/users/modify.go
+++ b/routers/api/v1/admin/users/modify.go
@@ -11,6 +11,7 @@ import (
type modifyReqStruct struct {
Password *string `json:"password"`
Role *int `json:"role"`
+ Verify *bool `json:"verify"`
}
func modifyHandler(ctx iris.Context) {
@@ -55,6 +56,10 @@ func modifyHandler(ctx iris.Context) {
user.Role = *req.Role
}
+ if req.Verify != nil {
+ user.Verify = *req.Verify
+ }
+
result = database.DB.Save(&user)
if result.Error != nil {
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
diff --git a/routers/api/v1/middleware/code.go b/routers/api/v1/middleware/code.go
index 730496d..df74601 100644
--- a/routers/api/v1/middleware/code.go
+++ b/routers/api/v1/middleware/code.go
@@ -42,6 +42,10 @@ const (
CodeAdminUserDeleteError ResCode = 2013
CodeAdminUserInstanceNoEmpty ResCode = 2014
CodeAdminUserModifyError ResCode = 2015
+
+ CodeUserAlreadyVerified ResCode = 3001
+ CodeUserVerifyInvalid ResCode = 3002
+ CodePasswordNotMatch ResCode = 3003
)
var codeMsgMap = map[ResCode]string{
@@ -84,4 +88,8 @@ var codeMsgMap = map[ResCode]string{
CodeAdminUserDeleteError: "delete user error",
CodeAdminUserModifyError: "modify user error",
CodeAdminUserInstanceNoEmpty: "user instances not empty",
+
+ CodeUserAlreadyVerified: "user already verified",
+ CodeUserVerifyInvalid: "email verify error",
+ CodePasswordNotMatch: "password not match",
}
diff --git a/routers/api/v1/user/login.go b/routers/api/v1/user/login.go
index 4d5852e..18b2830 100644
--- a/routers/api/v1/user/login.go
+++ b/routers/api/v1/user/login.go
@@ -65,6 +65,7 @@ func loginHandler(ctx iris.Context) {
Email: user.Email,
Role: user.Role,
Balance: user.Balance,
+ Verify: user.Verify,
}
middleware.Result(ctx, profile)
diff --git a/routers/api/v1/user/profile.go b/routers/api/v1/user/profile.go
index 934ee64..cd09e74 100644
--- a/routers/api/v1/user/profile.go
+++ b/routers/api/v1/user/profile.go
@@ -14,6 +14,7 @@ type profile struct {
Email string `json:"email"`
Role int `json:"role"`
Balance float64 `json:"balance"`
+ Verify bool `json:"verify"`
}
func profileHandler(ctx iris.Context) {
@@ -40,6 +41,7 @@ func profileHandler(ctx iris.Context) {
Email: user.Email,
Role: user.Role,
Balance: user.Balance,
+ Verify: user.Verify,
}
middleware.Result(ctx, profile)
diff --git a/routers/api/v1/user/resetPassword.go b/routers/api/v1/user/resetPassword.go
index 2c7ecbc..ca442ab 100644
--- a/routers/api/v1/user/resetPassword.go
+++ b/routers/api/v1/user/resetPassword.go
@@ -11,6 +11,7 @@ import (
type resetPasswordStruct struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
+ RePassword string `json:"re_password"`
}
func resetPasswordHandler(ctx iris.Context) {
@@ -22,12 +23,17 @@ func resetPasswordHandler(ctx iris.Context) {
return
}
- var resetPassword resetPasswordStruct
- if err := ctx.ReadJSON(&resetPassword); err != nil {
+ var req resetPasswordStruct
+ if err := ctx.ReadJSON(&req); err != nil {
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
return
}
+ if req.NewPassword != req.RePassword {
+ middleware.Error(ctx, middleware.CodePasswordNotMatch, iris.StatusBadRequest)
+ return
+ }
+
user := models.Users{
ID: uint(userId),
}
@@ -38,12 +44,12 @@ func resetPasswordHandler(ctx iris.Context) {
return
}
- if !user.CheckPassword(resetPassword.OldPassword) {
+ if !user.CheckPassword(req.OldPassword) {
middleware.Error(ctx, middleware.CodePasswordError, iris.StatusBadRequest)
return
}
- user.Password = user.PasswordHash(resetPassword.NewPassword)
+ user.Password = user.PasswordHash(req.NewPassword)
result = database.DB.Save(&user)
if result.Error != nil {
l.Error("save user error: %v", result.Error)
diff --git a/routers/api/v1/user/routers.go b/routers/api/v1/user/routers.go
index 2668309..ba80af0 100644
--- a/routers/api/v1/user/routers.go
+++ b/routers/api/v1/user/routers.go
@@ -25,6 +25,8 @@ func InitUser(party router.Party) {
party.Post("/register", registerHandler)
party.Get("/profile", middleware.AuthCheck, profileHandler)
party.Post("/resetPassword", middleware.AuthCheck, resetPasswordHandler)
+ party.Get("/verify/{code:string}", verifyHandler)
+ party.Post("/verify", middleware.AuthCheck, verifySendHandler)
servers.InitServers(party.Party("/servers"))
instances.InitInstances(party.Party("/instances"))
diff --git a/routers/api/v1/user/verify.go b/routers/api/v1/user/verify.go
new file mode 100644
index 0000000..0115683
--- /dev/null
+++ b/routers/api/v1/user/verify.go
@@ -0,0 +1,52 @@
+package user
+
+import (
+ "megrez/models"
+ "megrez/routers/api/v1/middleware"
+ "megrez/services/database"
+ "megrez/services/redis"
+
+ "github.com/kataras/iris/v12"
+)
+
+func verifyHandler(ctx iris.Context) {
+ l.SetFunction("verifyHandler")
+
+ code := ctx.Params().GetString("code")
+
+ rdb := redis.RawDB
+ v := rdb.Get(ctx, verifyRedisKeyPrefix+code)
+
+ if v.Err() != nil {
+ middleware.Error(ctx, middleware.CodeUserVerifyInvalid, iris.StatusBadRequest)
+ return
+ }
+
+ r := rdb.Del(ctx, verifyRedisKeyPrefix+code)
+ if r.Err() != nil {
+ middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
+ l.Error("delete redis verify code error: %v", r.Err())
+ return
+ }
+
+ email := v.Val()
+ l.Debug("verify email: %s", email)
+ user := models.Users{
+ Email: email,
+ }
+ result := database.DB.Where(&user).First(&user)
+ if result.Error != nil {
+ l.Error("get user error: %v", result.Error)
+ middleware.Error(ctx, middleware.CodeUserVerifyInvalid, iris.StatusBadRequest)
+ return
+ }
+
+ result = database.DB.Model(&user).Update("verify", true)
+ if result.Error != nil {
+ middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
+ l.Error("update user verify status Error: %v", result.Error)
+ return
+ }
+
+ middleware.Success(ctx)
+}
diff --git a/routers/api/v1/user/verifySend.go b/routers/api/v1/user/verifySend.go
new file mode 100644
index 0000000..63ebbab
--- /dev/null
+++ b/routers/api/v1/user/verifySend.go
@@ -0,0 +1,87 @@
+package user
+
+import (
+ "fmt"
+ "megrez/libs/crypto"
+ "megrez/models"
+ "megrez/routers/api/v1/middleware"
+ "megrez/services/config"
+ "megrez/services/database"
+ "megrez/services/redis"
+ "megrez/services/smtp"
+ "time"
+
+ "github.com/kataras/iris/v12"
+)
+
+const verifyRedisKeyPrefix = "verify:user:"
+const verifyUrlPrefix = "/verify/"
+const verifyTitle = "邮箱验证"
+const verifyHTMLFormat = `
+
+
+
+
+
+ MEGREZ 天权算能聚联计算平台
+
+
+
+
+
Hello, %s:
+
+ 请在15分钟内点击链接: %s 进行邮箱验证操作,十五分钟后该链接将会失效.
+
+
+ 如果您有任何问题,请联系系统管理员以获得更多信息与支持。
+
+
+
MEGREZ 天权算能聚联计算平台
+
+
+
+
+
+
+
+`
+
+func verifySendHandler(ctx iris.Context) {
+ l.SetFunction("verifySendHandler")
+
+ userId, err := ctx.Values().GetInt("userId")
+ if err != nil {
+ middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
+ return
+ }
+
+ user := models.Users{
+ ID: uint(userId),
+ }
+ result := database.DB.First(&user)
+ if result.Error != nil {
+ l.Error("get user error: %v", result.Error)
+ middleware.Error(ctx, middleware.CodeUserNotExist, iris.StatusInternalServerError)
+ return
+ }
+
+ if user.Verify {
+ middleware.Error(ctx, middleware.CodeUserAlreadyVerified, iris.StatusBadRequest)
+ return
+ }
+
+ rdb := redis.RawDB
+
+ verifyUrl := crypto.Hex(32)
+ rdb.Set(ctx, verifyRedisKeyPrefix+verifyUrl, user.Email, 15*time.Minute)
+
+ verifyUrl = config.GetSystemBaseUrl() + verifyUrlPrefix + verifyUrl
+ err = smtp.Send(user.Email, verifyTitle, fmt.Sprintf(verifyHTMLFormat, user.Username, verifyUrl, verifyUrl))
+ if err != nil {
+ middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
+ l.Error("Send SMTP Error: %v", err)
+ return
+ }
+
+ middleware.Success(ctx)
+}
diff --git a/services/config/config.go b/services/config/config.go
index 2d722f6..6781a86 100644
--- a/services/config/config.go
+++ b/services/config/config.go
@@ -9,6 +9,7 @@ type configStruct struct {
Http httpStruct `yaml:"http,omitempty"`
Database databaseStruct `yaml:"database,omitempty"`
Redis redisStruct `yaml:"redis,omitempty"`
+ Smtp smtpStruct `yaml:"smtp,omitempty"`
Log logStruct `yaml:"log,omitempty"`
System systemStruct `yaml:"system,omitempty"`
}
@@ -35,12 +36,21 @@ type redisStruct struct {
SentinelPassword string `yaml:"sentinel_password,omitempty"`
}
+type smtpStruct struct {
+ Host string `yaml:"host,omitempty"`
+ Port int `yaml:"port,omitempty"`
+ Password string `yaml:"password,omitempty"`
+ User string `yaml:"user,omitempty"`
+ SSL bool `yaml:"ssl,omitempty"`
+}
+
type logStruct struct {
Level string `yaml:"level,omitempty"`
File string `yaml:"file,omitempty"`
}
type systemStruct struct {
+ BaseUrl string `yaml:"base_url,omitempty"`
Salt string `yaml:"salt,omitempty"`
Verify bool `yaml:"verify,omitempty"`
MountDir string `yaml:"mount_dir,omitempty"`
@@ -64,6 +74,7 @@ var config = configStruct{
Password: "GpuManager",
Database: 0,
},
+ Smtp: smtpStruct{},
Log: logStruct{
Level: "DEBUG",
File: "data/logs/backend.log",
diff --git a/services/config/exporter.go b/services/config/exporter.go
index 2c7f5e1..17b73de 100644
--- a/services/config/exporter.go
+++ b/services/config/exporter.go
@@ -2,6 +2,7 @@ package config
import (
"os"
+ "strconv"
"gopkg.in/yaml.v3"
)
@@ -19,6 +20,10 @@ func InitConfig(path string) {
l.Fatal("Failed to unmarshal config file", err)
}
l.SetLevel(config.GetLogLevel())
+
+ if config.System.BaseUrl == "" {
+ config.System.BaseUrl = "http://" + config.Http.Host + ":" + strconv.Itoa(config.Http.Port)
+ }
}
func GetDatabase() databaseStruct {
@@ -33,6 +38,10 @@ func GetRedis() redisStruct {
return config.GetRedis()
}
+func GetSmtp() smtpStruct {
+ return config.GetSmtp()
+}
+
func GetLogLevel() string {
return config.GetLogLevel()
}
@@ -41,6 +50,10 @@ func GetLogFile() string {
return config.GetLogFile()
}
+func GetSystemBaseUrl() string {
+ return config.GetSystemBaseUrl()
+}
+
func GetSystemSalt() string {
return config.GetSystemSalt()
}
diff --git a/services/config/internal.go b/services/config/internal.go
index 256944c..9980e93 100644
--- a/services/config/internal.go
+++ b/services/config/internal.go
@@ -20,6 +20,10 @@ func (c *configStruct) GetRedis() redisStruct {
return c.Redis
}
+func (c *configStruct) GetSmtp() smtpStruct {
+ return c.Smtp
+}
+
func (c *configStruct) GetLogLevel() string {
return c.Log.Level
}
@@ -28,6 +32,10 @@ func (c *configStruct) GetLogFile() string {
return c.Log.File
}
+func (c *configStruct) GetSystemBaseUrl() string {
+ return c.System.BaseUrl
+}
+
func (c *configStruct) GetSystemSalt() string {
return c.System.Salt
}
diff --git a/services/smtp/smtp.go b/services/smtp/smtp.go
new file mode 100644
index 0000000..4e7854e
--- /dev/null
+++ b/services/smtp/smtp.go
@@ -0,0 +1,90 @@
+package smtp
+
+import (
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "megrez/services/config"
+ "net/mail"
+ "net/smtp"
+ "strconv"
+)
+
+const emailTitle = "MEGREZ 天权算能聚联计算平台"
+
+func Send(toAddr, title string, html string) error {
+ smtpConf := config.GetSmtp()
+
+ if smtpConf.Host == "" || smtpConf.Port == 0 || smtpConf.User == "" || smtpConf.Password == "" {
+ return errors.New("SMTP smtpConfiguration is not set")
+ }
+
+ if !smtpConf.SSL {
+ auth := smtp.PlainAuth("", smtpConf.User, smtpConf.Password, smtpConf.Host)
+ msg := append([]byte("Subject: "+title+" - "+emailTitle+" \r\n"+
+ "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\r\n\r\n"),
+ html...)
+ err := smtp.SendMail(smtpConf.Host+":"+strconv.Itoa(smtpConf.Port), auth, smtpConf.User, []string{toAddr}, msg)
+ if err != nil {
+ return errors.New("fail to send email, Error:" + err.Error())
+ }
+ return nil
+ }
+
+ from := mail.Address{Name: "MEGREZ", Address: smtpConf.User}
+ to := mail.Address{Name: "", Address: toAddr}
+ auth := smtp.PlainAuth("", smtpConf.User, smtpConf.Password, smtpConf.Host)
+ // Setup headers
+ headers := make(map[string]string)
+ headers["From"] = from.String()
+ headers["To"] = to.String()
+ headers["Subject"] = title + " - " + emailTitle
+ headers["Content-Type"] = "text/html;charset=UTF-8"
+ msg := ""
+ for k, v := range headers {
+ msg += fmt.Sprintf("%s: %s\r\n", k, v)
+ }
+ msg += "\r\n" + html
+ tlssmtpConfig := &tls.Config{
+ InsecureSkipVerify: true,
+ ServerName: smtpConf.Host,
+ }
+ conn, err := tls.Dial("tcp", smtpConf.Host+":"+strconv.Itoa(smtpConf.Port), tlssmtpConfig)
+ if err != nil {
+ return errors.New("fail to connect to the server, Error:" + err.Error())
+ }
+ c, err := smtp.NewClient(conn, smtpConf.Host)
+ if err != nil {
+ return errors.New("fail to create smtp client, Error:" + err.Error())
+ }
+ if err = c.Auth(auth); err != nil {
+ return errors.New("fail to auth, Error:" + err.Error())
+ }
+
+ // To && From
+ if err = c.Mail(from.Address); err != nil {
+ return errors.New("fail to set from address, Error:" + err.Error())
+ }
+
+ if err = c.Rcpt(to.Address); err != nil {
+ return errors.New("fail to set to address, Error:" + err.Error())
+ }
+ w, err := c.Data()
+ if err != nil {
+ return errors.New("fail to get smtp data writer, Error:" + err.Error())
+ }
+
+ _, err = w.Write([]byte(msg))
+ if err != nil {
+ return errors.New("fail to write data, Error:" + err.Error())
+ }
+
+ err = w.Close()
+ if err != nil {
+ return errors.New("fail to close smtp data writer, Error:" + err.Error())
+ }
+
+ c.Quit()
+
+ return nil
+}
diff --git a/services/system/init.go b/services/system/init.go
index 79a13eb..3d3075a 100644
--- a/services/system/init.go
+++ b/services/system/init.go
@@ -24,6 +24,7 @@ func systemInit() (err error) {
Username: "admin",
Email: "admin@gpuManager.com",
Role: 3,
+ Verify: true,
}
result := database.DB.Create(&user)
if result.Error != nil {