mirror of
https://github.com/XShengTech/MEGREZ.git
synced 2026-01-14 09:07:16 +08:00
[Feat] ✨ Add ForgetPassword API & Pages #15
This commit is contained in:
parent
801335ab5d
commit
35a55a9e03
@ -35,6 +35,12 @@ export default {
|
||||
UserRegister(data) {
|
||||
return ajax('user/register', 'post', { data })
|
||||
},
|
||||
UserForgetRequest(data) {
|
||||
return ajax('user/forget', 'post', { data })
|
||||
},
|
||||
UserForgerPassword(data) {
|
||||
return ajax('user/password', 'put', { data })
|
||||
},
|
||||
UserResetPassword(data) {
|
||||
return ajax('user/password', 'post', { data })
|
||||
},
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import AppLayout from '@/layout/AppLayout.vue';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
import Forget from '@/views/Forget.vue';
|
||||
import Login from '@/views/Login.vue';
|
||||
import Register from '@/views/Register.vue';
|
||||
import Reset from '@/views/Reset.vue';
|
||||
import Verify from '@/views/Verify.vue';
|
||||
|
||||
import InstanceCreate from '@/views/users/InstanceCreate.vue';
|
||||
@ -33,6 +35,16 @@ const router = createRouter({
|
||||
name: 'register',
|
||||
component: Register
|
||||
},
|
||||
{
|
||||
path: '/forget',
|
||||
name: 'forget',
|
||||
component: Forget
|
||||
},
|
||||
{
|
||||
path: '/reset/:code',
|
||||
name: 'forget-reset',
|
||||
component: Reset
|
||||
},
|
||||
{
|
||||
path: '/verify/:code',
|
||||
name: 'verify',
|
||||
|
||||
71
frontend/src/views/Forget.vue
Normal file
71
frontend/src/views/Forget.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<FloatingConfigurator />
|
||||
<div
|
||||
class="bg-surface-50 dark:bg-surface-950 flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div
|
||||
style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
|
||||
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20" style="border-radius: 53px">
|
||||
<div class="text-center items-center flex flex-col mb-8">
|
||||
<img :src="logo" style="width: 16rem;" />
|
||||
<div class="text-surface-900 dark:text-surface-0 text-3xl font-medium mt-6 mb-4">天权 算能聚联计算平台</div>
|
||||
<span class="text-muted-color font-medium">忘记密码</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-2">邮箱</label>
|
||||
<InputText id="email" type="text" placeholder="邮箱" class="w-full md:w-[30rem] mb-8" v-model="form.email" />
|
||||
|
||||
<div class="flex items-center justify-between mt-2 mb-8 gap-8">
|
||||
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-slate-600">记起密码?<span
|
||||
class="text-primary" @click="handleLogin">立即登入</span></span>
|
||||
</div>
|
||||
<Button label="发送邮件" class="w-full" @click="handleSubmit"></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import logo from '@/assets/logo-text.webp';
|
||||
import FloatingConfigurator from '@/components/FloatingConfigurator.vue';
|
||||
|
||||
import api from '@/api';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
|
||||
const form = ref({
|
||||
email: ''
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
api.UserForgetRequest(form.value).then(res => {
|
||||
toast.add({ severity: 'success', summary: '发送成功', detail: '请查看邮箱', life: 3000 })
|
||||
}).catch(err => {
|
||||
console.error(err)
|
||||
toast.add({ severity: 'error', summary: '发送失败', detail: '请检查后重新尝试', life: 3000 })
|
||||
})
|
||||
}
|
||||
|
||||
const handleLogin = () => {
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pi-eye {
|
||||
transform: scale(1.6);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.pi-eye-slash {
|
||||
transform: scale(1.6);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
@ -25,7 +25,8 @@
|
||||
<div class="flex items-center justify-between mt-2 mb-8 gap-8">
|
||||
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-slate-600">没有账号?<span
|
||||
class="text-primary" @click="handleRegister">立即注册</span></span>
|
||||
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-primary">忘记密码</span>
|
||||
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-primary"
|
||||
@click="handleForget">忘记密码</span>
|
||||
</div>
|
||||
<Button label="登入" class="w-full" @click="handleSubmit"></Button>
|
||||
</div>
|
||||
@ -69,6 +70,10 @@ const handleSubmit = () => {
|
||||
const handleRegister = () => {
|
||||
router.push('/register')
|
||||
}
|
||||
|
||||
const handleForget = () => {
|
||||
router.push('/forget')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
102
frontend/src/views/Reset.vue
Normal file
102
frontend/src/views/Reset.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<FloatingConfigurator />
|
||||
<div
|
||||
class="bg-surface-50 dark:bg-surface-950 flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div
|
||||
style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
|
||||
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20" style="border-radius: 53px">
|
||||
<div class="text-center items-center flex flex-col mb-8">
|
||||
<img :src="logo" style="width: 16rem;" />
|
||||
<div class="text-surface-900 dark:text-surface-0 text-3xl font-medium mt-6 mb-4">天权 算能聚联计算平台</div>
|
||||
<span class="text-muted-color font-medium">重置密码</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password"
|
||||
class="block text-surface-900 dark:text-surface-0 font-medium text-xl mb-2">新密码</label>
|
||||
<Password id="password" v-model="form.password" placeholder="新密码" :toggleMask="true"
|
||||
class="w-full md:w-[30rem] mb-4" fluid :feedback="false" />
|
||||
|
||||
<label for="repassword"
|
||||
class="block text-surface-900 dark:text-surface-0 font-medium text-xl mb-2">重复密码</label>
|
||||
<Password id="repassword" v-model="form.repassword" placeholder="重复密码" :toggleMask="true" class="mb-4" fluid
|
||||
:feedback="false" @keydown.enter="handleSubmit" />
|
||||
|
||||
<div class="flex items-center justify-between mt-2 mb-8 gap-8">
|
||||
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-slate-600">记起密码?<span
|
||||
class="text-primary" @click="handleLogin">立即登入</span></span>
|
||||
</div>
|
||||
<Button label="注册" class="w-full" @click="handleSubmit"></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import logo from '@/assets/logo-text.webp';
|
||||
import FloatingConfigurator from '@/components/FloatingConfigurator.vue';
|
||||
|
||||
import api from '@/api';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
|
||||
const form = ref({
|
||||
code: route.params.code,
|
||||
password: '',
|
||||
repassword: ''
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!form.value.code) {
|
||||
toast.add({ severity: 'error', summary: '验证码错误', detail: '请检查后重新尝试', life: 3000 })
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.password) {
|
||||
toast.add({ severity: 'error', summary: '密码不能为空', detail: '请检查后重新尝试', life: 3000 })
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.value.repassword) {
|
||||
toast.add({ severity: 'error', summary: '重复密码不能为空', detail: '请检查后重新尝试', life: 3000 })
|
||||
return
|
||||
}
|
||||
|
||||
if (form.value.password !== form.value.repassword) {
|
||||
toast.add({ severity: 'error', summary: '两次密码不一致', detail: '请检查后重新尝试', life: 3000 })
|
||||
return
|
||||
}
|
||||
|
||||
api.UserForgerPassword(form.value).then(res => {
|
||||
toast.add({ severity: 'success', summary: '重置密码成功', detail: '请登录', life: 3000 })
|
||||
router.push('/login')
|
||||
}).catch(err => {
|
||||
console.error(err)
|
||||
toast.add({ severity: 'error', summary: '重置密码失败', detail: '请检查后重新尝试', life: 3000 })
|
||||
})
|
||||
}
|
||||
|
||||
const handleLogin = () => {
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pi-eye {
|
||||
transform: scale(1.6);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.pi-eye-slash {
|
||||
transform: scale(1.6);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
@ -49,7 +49,7 @@ func modifyHandler(ctx iris.Context) {
|
||||
}
|
||||
|
||||
if req.Email != nil {
|
||||
if *req.Email != "" && utils.EmailFormat(*req.Email) {
|
||||
if *req.Email != "" && !utils.EmailFormat(*req.Email) {
|
||||
user.Email = *req.Email
|
||||
}
|
||||
}
|
||||
|
||||
78
routers/api/v1/user/forgerPassword.go
Normal file
78
routers/api/v1/user/forgerPassword.go
Normal file
@ -0,0 +1,78 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"megrez/models"
|
||||
"megrez/routers/api/v1/middleware"
|
||||
"megrez/services/database"
|
||||
"megrez/services/redis"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
type forgerPasswordStruct struct {
|
||||
Code string `json:"code"`
|
||||
Password string `json:"password"`
|
||||
RePassword string `json:"repassword"`
|
||||
}
|
||||
|
||||
func forgetPasswordHandler(ctx iris.Context) {
|
||||
l.SetFunction("forgetPasswordHandler")
|
||||
|
||||
var req forgerPasswordStruct
|
||||
if err := ctx.ReadJSON(&req); err != nil {
|
||||
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Code == "" || req.Password == "" || req.RePassword == "" {
|
||||
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password != req.RePassword {
|
||||
middleware.Error(ctx, middleware.CodePasswordNotMatch, iris.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rdb := redis.RawDB
|
||||
v := rdb.Get(ctx, forgetPasswordRedisKeyPrefix+req.Code)
|
||||
|
||||
if v.Err() != nil {
|
||||
middleware.Error(ctx, middleware.CodeUserVerifyInvalid, iris.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
r := rdb.Del(ctx, forgetPasswordRedisKeyPrefix+req.Code)
|
||||
if r.Err() != nil {
|
||||
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
|
||||
l.Error("delete redis verify code error: %v", r.Err())
|
||||
return
|
||||
}
|
||||
|
||||
id, err := v.Int()
|
||||
if err != nil {
|
||||
middleware.Error(ctx, middleware.CodeUserVerifyInvalid, iris.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user := models.Users{
|
||||
ID: uint(id),
|
||||
}
|
||||
result := database.DB.First(&user)
|
||||
if result.Error != nil {
|
||||
l.Error("get user error: %v", result.Error)
|
||||
middleware.Error(ctx, middleware.CodeUserVerifyInvalid, iris.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = user.PasswordHash(req.Password)
|
||||
|
||||
result = database.DB.Model(&user).Update("password", user.Password)
|
||||
if result.Error != nil {
|
||||
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
|
||||
l.Error("update user password Error: %v", result.Error)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.Success(ctx)
|
||||
}
|
||||
97
routers/api/v1/user/forgetSend.go
Normal file
97
routers/api/v1/user/forgetSend.go
Normal file
@ -0,0 +1,97 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type forgetSendStruct struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
const forgetPasswordRedisKeyPrefix = "forget:user:"
|
||||
const forgetPasswordUrlPrefix = "/reset/"
|
||||
const forgetPasswordTitle = "重置密码"
|
||||
const forgetPasswordHTMLFormat = `
|
||||
<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-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 forgetSendHandler(ctx iris.Context) {
|
||||
var req forgetSendStruct
|
||||
if err := ctx.ReadJSON(&req); err != nil {
|
||||
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email == "" {
|
||||
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user := models.Users{
|
||||
Email: req.Email,
|
||||
}
|
||||
result := database.DB.Where(&user).First(&user)
|
||||
if result.Error != nil {
|
||||
l.Error("get user error: %v", result.Error)
|
||||
middleware.Error(ctx, middleware.CodeUserNotExist, iris.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rdb := redis.RawDB
|
||||
|
||||
forgetUrl := crypto.Hex(32)
|
||||
err := rdb.Set(ctx, forgetPasswordRedisKeyPrefix+forgetUrl, user.ID, 15*time.Minute).Err()
|
||||
if err != nil {
|
||||
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
|
||||
l.Error("Set Redis error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
forgetUrl = config.GetSystemBaseUrl() + forgetPasswordUrlPrefix + forgetUrl
|
||||
err = smtp.Send(user.Email, forgetPasswordTitle, fmt.Sprintf(forgetPasswordHTMLFormat, user.Username, forgetUrl, forgetUrl))
|
||||
if err != nil {
|
||||
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
|
||||
l.Error("Send SMTP Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.Success(ctx)
|
||||
}
|
||||
@ -25,6 +25,8 @@ func InitUser(party router.Party) {
|
||||
party.Post("/register", registerHandler)
|
||||
party.Get("/profile", middleware.AuthCheck, profileHandler)
|
||||
party.Post("/password", middleware.AuthCheck, resetPasswordHandler)
|
||||
party.Post("/forget", forgetSendHandler)
|
||||
party.Put("/password", forgetPasswordHandler)
|
||||
party.Post("/email", middleware.AuthCheck, resetEmailHandler)
|
||||
party.Get("/verify/{code:string}", verifyHandler)
|
||||
party.Post("/verify", middleware.AuthCheck, verifySendHandler)
|
||||
|
||||
@ -30,7 +30,6 @@ func verifyHandler(ctx iris.Context) {
|
||||
}
|
||||
|
||||
email := v.Val()
|
||||
l.Debug("verify email: %s", email)
|
||||
user := models.Users{
|
||||
Email: email,
|
||||
}
|
||||
|
||||
@ -73,7 +73,12 @@ func verifySendHandler(ctx iris.Context) {
|
||||
rdb := redis.RawDB
|
||||
|
||||
verifyUrl := crypto.Hex(32)
|
||||
rdb.Set(ctx, verifyRedisKeyPrefix+verifyUrl, user.Email, 15*time.Minute)
|
||||
err = rdb.Set(ctx, verifyRedisKeyPrefix+verifyUrl, user.Email, 15*time.Minute).Err()
|
||||
if err != nil {
|
||||
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
|
||||
l.Error("Set Redis Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
verifyUrl = config.GetSystemBaseUrl() + verifyUrlPrefix + verifyUrl
|
||||
err = smtp.Send(user.Email, verifyTitle, fmt.Sprintf(verifyHTMLFormat, user.Username, verifyUrl, verifyUrl))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user