mirror of
https://github.com/XShengTech/MEGREZ.git
synced 2026-01-13 16:47:16 +08:00
[Feat] ✨ Add Email Verify API & Pages #15
This commit is contained in:
parent
52bdfa17eb
commit
2e3cfbcecb
@ -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
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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',
|
||||
|
||||
91
frontend/src/views/Verify.vue
Normal file
91
frontend/src/views/Verify.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<FloatingConfigurator />
|
||||
<div class="flex items-center justify-center min-h-screen overflow-hidden">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<img :src="logo" width="64" height="32" class="-mt-6 mb-4" />
|
||||
<div
|
||||
style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, color-mix(in srgb, var(--primary-color), transparent 60%) 10%, var(--surface-ground) 30%)">
|
||||
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20 flex flex-col items-center"
|
||||
style="border-radius: 53px">
|
||||
<h1 class="text-surface-900 dark:text-surface-0 font-bold text-3xl lg:text-5xl mb-2">邮箱验证</h1>
|
||||
<div class="text-surface-600 dark:text-surface-200 mb-8">验证邮箱地址</div>
|
||||
|
||||
<router-link v-if="status == 0"
|
||||
class="w-full flex items-center mb-8 py-8 border-surface-300 dark:border-surface-500 border-b">
|
||||
<span class="flex justify-center items-center border-2 border-primary text-primary rounded-border"
|
||||
style="height: 3.5rem; width: 3.5rem">
|
||||
<i class="pi pi-fw pi-spin pi-spinner !text-2xl"></i>
|
||||
</span>
|
||||
<span class="ml-6 flex flex-col">
|
||||
<span class="text-surface-900 dark:text-surface-0 lg:text-xl font-medium mb-0">
|
||||
验证中
|
||||
</span>
|
||||
<span class="text-surface-600 dark:text-surface-200 lg:text-xl">
|
||||
请稍后
|
||||
</span>
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-else-if="status == 1" to="/"
|
||||
class="w-full flex items-center mb-8 py-8 border-surface-300 dark:border-surface-500 border-b">
|
||||
<span class="flex justify-center items-center border-2 border-emerald-500 text-emerald-500 rounded-border"
|
||||
style="height: 3.5rem; width: 3.5rem">
|
||||
<i class="pi pi-fw pi-check !text-2xl"></i>
|
||||
</span>
|
||||
<span class="ml-6 flex flex-col">
|
||||
<span class="text-surface-900 dark:text-surface-0 lg:text-xl font-medium mb-0">
|
||||
验证成功
|
||||
</span>
|
||||
<span class="text-surface-600 dark:text-surface-200 lg:text-xl">
|
||||
你可以开始使用了
|
||||
</span>
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-else-if="status == 2" to="/"
|
||||
class="w-full flex items-center mb-8 py-8 border-surface-300 dark:border-surface-500 border-b">
|
||||
<span class="flex justify-center items-center border-2 border-red-500 text-red-500 rounded-border"
|
||||
style="height: 3.5rem; width: 3.5rem">
|
||||
<i class="pi pi-fw pi-times !text-2xl"></i>
|
||||
</span>
|
||||
<span class="ml-6 flex flex-col">
|
||||
<span class="text-surface-900 dark:text-surface-0 lg:text-xl font-medium mb-0">
|
||||
验证失败
|
||||
</span>
|
||||
<span class="text-surface-600 dark:text-surface-200 lg:text-xl">
|
||||
请重新验证
|
||||
</span>
|
||||
</span>
|
||||
</router-link>
|
||||
<Button as="router-link" label="回到仪表盘" to="/" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import logo from '@/assets/logo.svg';
|
||||
import FloatingConfigurator from '@/components/FloatingConfigurator.vue';
|
||||
|
||||
import api from '@/api';
|
||||
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
|
||||
const status = ref(0) // 0: verifing, 1: success, 2: fail
|
||||
const code = route.params.code
|
||||
|
||||
api.UserVerify(code).then(res => {
|
||||
status.value = 1
|
||||
toast.add({ severity: 'success', summary: '验证成功', detail: '你可以开始使用了', life: 3000 })
|
||||
}).catch(err => {
|
||||
status.value = 2
|
||||
toast.add({ severity: 'error', summary: '验证失败', detail: err.response.data.msg, life: 3000 })
|
||||
console.error(err)
|
||||
})
|
||||
</script>
|
||||
@ -1,3 +1,105 @@
|
||||
<template>
|
||||
个人设置
|
||||
<SectionBanner label="安全设置" icon="pi pi-cog text-emerald-500"></SectionBanner>
|
||||
<Fluid>
|
||||
<div class="flex flex-col md:flex-row gap-8">
|
||||
<div class="md:w-1/2">
|
||||
<div class="card rounded-2xl flex flex-col gap-4">
|
||||
<div class="font-semibold text-xl">邮箱验证</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="email">邮箱</label>
|
||||
<InputText v-model="userProfile.email" id="email" type="text" disabled />
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<Button v-if="!userProfile.verify && !verifyRequesting" label="验证" severity="warn"
|
||||
style="width: 5.6rem; float: right;" @click="emailVerify" />
|
||||
<Button v-else-if="verifyRequesting" label="发送中" severity="warn" icon="pi pi-spin pi-spinner" disabled
|
||||
style="width: 7.2rem; float: right;" />
|
||||
<Button v-else label="已验证" severity="secondary" disabled style="width: 5.6rem; float: right;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card rounded-2xl flex flex-col gap-4">
|
||||
<div class="font-semibold text-xl">修改邮箱</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="email">邮箱</label>
|
||||
<InputText v-if="!emailModifyStatus" v-model="userProfile.email" id="email" type="text" disabled />
|
||||
<InputText v-else v-model="emailModifyValue" id="email" type="text" />
|
||||
</div>
|
||||
<div v-if="!emailModifyStatus" class="mt-2">
|
||||
<Button label="修改" style="width: 5.6rem; float: right;" @click="emailModifyStatus = true" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<Button label="保存" style="width: 5.6rem; float: right;" />
|
||||
<Button class="mr-2" label="取消" severity="secondary" style="width: 5.6rem; float: right;"
|
||||
@click="emailModifyStatus = false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:w-1/2">
|
||||
<div class="card rounded-2xl flex flex-col gap-4">
|
||||
<div class="font-semibold text-xl">修改密码</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="old_password">原密码</label>
|
||||
<InputText id="old_password" type="text" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="new_password">新密码</label>
|
||||
<InputText id="new_password" type="text" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="re_password">重复密码</label>
|
||||
<InputText id="re_password" type="text" />
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<Button label="保存" style="width: 5.6rem; float: right;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fluid>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import api from '@/api';
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const userProfile = ref({})
|
||||
|
||||
const verifyRequesting = ref(false)
|
||||
|
||||
const emailModifyStatus = ref(false)
|
||||
const emailModifyValue = ref('')
|
||||
|
||||
const getProfile = () => {
|
||||
api.GetUserProfile().then((res) => {
|
||||
userProfile.value = res.data.data.result
|
||||
emailModifyValue.value = userProfile.value.email
|
||||
console.log(userProfile.value)
|
||||
}).catch((err) => {
|
||||
toast.add({ severity: 'error', summary: '获取用户信息失败', detail: err.response.data.msg, life: 3000 })
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
const emailVerify = () => {
|
||||
verifyRequesting.value = true
|
||||
api.UserVerifyRequest().then((res) => {
|
||||
toast.add({ severity: 'success', summary: '验证邮件已发送', detail: '请前往邮箱查看', life: 3000 })
|
||||
verifyRequesting.value = false
|
||||
}).catch((err) => {
|
||||
toast.add({ severity: 'error', summary: '发送失败', detail: err.response.data.msg, life: 3000 })
|
||||
verifyRequesting.value = false
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getProfile()
|
||||
})
|
||||
|
||||
</script>
|
||||
@ -17,6 +17,7 @@ type Users struct {
|
||||
Role int `json:"role" gorm:"not null,default:0"`
|
||||
|
||||
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"`
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"))
|
||||
|
||||
52
routers/api/v1/user/verify.go
Normal file
52
routers/api/v1/user/verify.go
Normal file
@ -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)
|
||||
}
|
||||
87
routers/api/v1/user/verifySend.go
Normal file
87
routers/api/v1/user/verifySend.go
Normal file
@ -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 = `
|
||||
<div>
|
||||
<table cellpadding="0" align="center" style="overflow:hidden;background:#fff;margin:0 auto;text-align:left;position:relative;font-size:14px; font-family:'lucida Grande',Verdana;line-height:1.5;box-shadow:0 0 3px #ccc;border:1px solid #ccc;border-radius:5px;border-collapse:collapse;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th valign="middle" style="height:38px;color:#fff; font-size:14px;line-height:38px; font-weight:bold;text-align:left;padding:10px 24px 6px; border-bottom:1px solid #467ec3;background:#518bcb;border-radius:5px 5px 0 0;">
|
||||
MEGREZ 天权算能聚联计算平台</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="padding:20px 35px 40px;">
|
||||
<h2 style="font-weight:bold;margin-bottom:5px;font-size:14px;">Hello, %s:</h2>
|
||||
<p style="margin-top:20px">
|
||||
请在15分钟内点击链接: <a href="%s">%s</a> 进行邮箱验证操作,十五分钟后该链接将会失效.
|
||||
</p>
|
||||
<p style="margin-top:20px">
|
||||
如果您有任何问题,请联系系统管理员以获得更多信息与支持。
|
||||
</p>
|
||||
<p style="margin-left:2em;"></p>
|
||||
<p style="text-indent:0;text-align:right;">MEGREZ 天权算能聚联计算平台</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`
|
||||
|
||||
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)
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
90
services/smtp/smtp.go
Normal file
90
services/smtp/smtp.go
Normal file
@ -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
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user