mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 11:06:23 +00:00
feat: add email verification and password reset functionality
- Introduced environment variables for database and email configurations. - Implemented email verification code generation and validation. - Added password reset feature with email verification. - Updated user registration and profile management APIs. - Refactored user security settings to include email and password updates. - Enhanced console layout with internationalization support. - Removed deprecated settings page and integrated global settings. - Added new reset password page and form components. - Updated localization files for new features and translations.
This commit is contained in:
@ -1,129 +1,129 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"context"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/snowykami/neo-blog/internal/ctxutils"
|
||||
"github.com/snowykami/neo-blog/internal/model"
|
||||
"github.com/snowykami/neo-blog/internal/repo"
|
||||
"github.com/snowykami/neo-blog/pkg/filedriver"
|
||||
"github.com/snowykami/neo-blog/pkg/resps"
|
||||
"github.com/snowykami/neo-blog/pkg/utils"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/snowykami/neo-blog/internal/ctxutils"
|
||||
"github.com/snowykami/neo-blog/internal/model"
|
||||
"github.com/snowykami/neo-blog/internal/repo"
|
||||
"github.com/snowykami/neo-blog/pkg/filedriver"
|
||||
"github.com/snowykami/neo-blog/pkg/resps"
|
||||
"github.com/snowykami/neo-blog/pkg/utils"
|
||||
)
|
||||
|
||||
type FileController struct{}
|
||||
|
||||
func NewFileController() *FileController {
|
||||
return &FileController{}
|
||||
return &FileController{}
|
||||
}
|
||||
|
||||
func (f *FileController) UploadFileStream(ctx context.Context, c *app.RequestContext) {
|
||||
// 获取文件信息
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
logrus.Error("无法读取文件: ", err)
|
||||
resps.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 获取文件信息
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
logrus.Error("无法读取文件: ", err)
|
||||
resps.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
group := string(c.FormValue("group"))
|
||||
name := string(c.FormValue("name"))
|
||||
group := string(c.FormValue("group"))
|
||||
name := string(c.FormValue("name"))
|
||||
|
||||
// 初始化文件驱动
|
||||
driver, err := filedriver.GetFileDriver(filedriver.GetWebdavDriverConfig())
|
||||
if err != nil {
|
||||
logrus.Error("获取文件驱动失败: ", err)
|
||||
resps.InternalServerError(c, "获取文件驱动失败")
|
||||
return
|
||||
}
|
||||
// 初始化文件驱动
|
||||
driver, err := filedriver.GetFileDriver(filedriver.GetWebdavDriverConfig())
|
||||
if err != nil {
|
||||
logrus.Error("获取文件驱动失败: ", err)
|
||||
resps.InternalServerError(c, "获取文件驱动失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 校验文件哈希
|
||||
if hashForm := string(c.FormValue("hash")); hashForm != "" {
|
||||
dir, fileName := utils.FilePath(hashForm)
|
||||
storagePath := filepath.Join(dir, fileName)
|
||||
if _, err := driver.Stat(c, storagePath); err == nil {
|
||||
resps.Ok(c, "文件已存在", map[string]any{"hash": hashForm})
|
||||
return
|
||||
}
|
||||
}
|
||||
// 校验文件哈希
|
||||
if hashForm := string(c.FormValue("hash")); hashForm != "" {
|
||||
dir, fileName := utils.FilePath(hashForm)
|
||||
storagePath := filepath.Join(dir, fileName)
|
||||
if _, err := driver.Stat(c, storagePath); err == nil {
|
||||
resps.Ok(c, "文件已存在", map[string]any{"hash": hashForm})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
logrus.Error("无法打开文件: ", err)
|
||||
resps.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
logrus.Error("无法打开文件: ", err)
|
||||
resps.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 计算文件哈希值
|
||||
hash, err := utils.FileHashFromStream(src)
|
||||
if err != nil {
|
||||
logrus.Error("计算文件哈希失败: ", err)
|
||||
resps.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 计算文件哈希值
|
||||
hash, err := utils.FileHashFromStream(src)
|
||||
if err != nil {
|
||||
logrus.Error("计算文件哈希失败: ", err)
|
||||
resps.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 根据哈希值生成存储路径
|
||||
dir, fileName := utils.FilePath(hash)
|
||||
storagePath := filepath.Join(dir, fileName)
|
||||
// 保存文件
|
||||
if _, err := src.Seek(0, io.SeekStart); err != nil {
|
||||
logrus.Error("无法重置文件流位置: ", err)
|
||||
resps.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := driver.Save(c, storagePath, src); err != nil {
|
||||
logrus.Error("保存文件失败: ", err)
|
||||
resps.InternalServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 数据库索引建立
|
||||
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
resps.InternalServerError(c, "获取当前用户失败")
|
||||
return
|
||||
}
|
||||
fileModel := &model.File{
|
||||
Hash: hash,
|
||||
UserID: currentUser.ID,
|
||||
Group: group,
|
||||
Name: name,
|
||||
}
|
||||
// 根据哈希值生成存储路径
|
||||
dir, fileName := utils.FilePath(hash)
|
||||
storagePath := filepath.Join(dir, fileName)
|
||||
// 保存文件
|
||||
if _, err := src.Seek(0, io.SeekStart); err != nil {
|
||||
logrus.Error("无法重置文件流位置: ", err)
|
||||
resps.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := driver.Save(c, storagePath, src); err != nil {
|
||||
logrus.Error("保存文件失败: ", err)
|
||||
resps.InternalServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 数据库索引建立
|
||||
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
||||
if !ok {
|
||||
resps.InternalServerError(c, "获取当前用户失败")
|
||||
return
|
||||
}
|
||||
fileModel := &model.File{
|
||||
Hash: hash,
|
||||
UserID: currentUser.ID,
|
||||
Group: group,
|
||||
Name: name,
|
||||
}
|
||||
|
||||
if err := repo.File.Create(fileModel); err != nil {
|
||||
logrus.Error("数据库索引建立失败: ", err)
|
||||
resps.InternalServerError(c, "数据库索引建立失败")
|
||||
return
|
||||
}
|
||||
resps.Ok(c, "文件上传成功", map[string]any{"hash": hash, "id": fileModel.ID})
|
||||
if err := repo.File.Create(fileModel); err != nil {
|
||||
logrus.Error("数据库索引建立失败: ", err)
|
||||
resps.InternalServerError(c, "数据库索引建立失败")
|
||||
return
|
||||
}
|
||||
resps.Ok(c, "文件上传成功", map[string]any{"hash": hash, "id": fileModel.ID})
|
||||
}
|
||||
|
||||
func (f *FileController) GetFile(ctx context.Context, c *app.RequestContext) {
|
||||
fileIdString := c.Param("id")
|
||||
fileId, err := strconv.ParseUint(fileIdString, 10, 64)
|
||||
if err != nil {
|
||||
logrus.Error("无效的文件ID: ", err)
|
||||
resps.BadRequest(c, "无效的文件ID")
|
||||
return
|
||||
}
|
||||
fileModel, err := repo.File.GetByID(uint(fileId))
|
||||
if err != nil {
|
||||
logrus.Error("获取文件信息失败: ", err)
|
||||
resps.InternalServerError(c, "获取文件信息失败")
|
||||
return
|
||||
}
|
||||
driver, err := filedriver.GetFileDriver(filedriver.GetWebdavDriverConfig())
|
||||
if err != nil {
|
||||
logrus.Error("获取文件驱动失败: ", err)
|
||||
resps.InternalServerError(c, "获取文件驱动失败")
|
||||
return
|
||||
}
|
||||
filePath := filepath.Join(utils.FilePath(fileModel.Hash))
|
||||
driver.Get(c, filePath)
|
||||
fileIdString := c.Param("id")
|
||||
fileId, err := strconv.ParseUint(fileIdString, 10, 64)
|
||||
if err != nil {
|
||||
logrus.Error("无效的文件ID: ", err)
|
||||
resps.BadRequest(c, "无效的文件ID")
|
||||
return
|
||||
}
|
||||
fileModel, err := repo.File.GetByID(uint(fileId))
|
||||
if err != nil {
|
||||
logrus.Error("获取文件信息失败: ", err)
|
||||
resps.InternalServerError(c, "获取文件信息失败")
|
||||
return
|
||||
}
|
||||
driver, err := filedriver.GetFileDriver(filedriver.GetWebdavDriverConfig())
|
||||
if err != nil {
|
||||
logrus.Error("获取文件驱动失败: ", err)
|
||||
resps.InternalServerError(c, "获取文件驱动失败")
|
||||
return
|
||||
}
|
||||
filePath := filepath.Join(utils.FilePath(fileModel.Hash))
|
||||
driver.Get(c, filePath)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package v1
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"github.com/cloudwego/hertz/pkg/common/utils"
|
||||
@ -184,6 +185,9 @@ func (u *UserController) VerifyEmail(ctx context.Context, c *app.RequestContext)
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
}
|
||||
if verifyEmailReq.Email == "" {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
}
|
||||
resp, err := u.service.RequestVerifyEmail(&verifyEmailReq)
|
||||
if err != nil {
|
||||
serviceErr := errs.AsServiceError(err)
|
||||
@ -194,11 +198,63 @@ func (u *UserController) VerifyEmail(ctx context.Context, c *app.RequestContext)
|
||||
}
|
||||
|
||||
func (u *UserController) ChangePassword(ctx context.Context, c *app.RequestContext) {
|
||||
// TODO: 实现修改密码功能
|
||||
var updatePasswordReq dto.UpdatePasswordReq
|
||||
if err := c.BindAndValidate(&updatePasswordReq); err != nil {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
}
|
||||
ok, err := u.service.UpdatePassword(ctx, &updatePasswordReq)
|
||||
if err != nil {
|
||||
resps.InternalServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
resps.BadRequest(c, "Failed to change password")
|
||||
return
|
||||
}
|
||||
resps.Ok(c, resps.Success, nil)
|
||||
}
|
||||
|
||||
func (u *UserController) ResetPassword(ctx context.Context, c *app.RequestContext) {
|
||||
var resetPasswordReq dto.ResetPasswordReq
|
||||
if err := c.BindAndValidate(&resetPasswordReq); err != nil {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
return
|
||||
}
|
||||
email := strings.TrimSpace(string(c.GetHeader(constant.HeaderKeyEmail)))
|
||||
if email == "" {
|
||||
resps.BadRequest(c, "Email header is required")
|
||||
return
|
||||
}
|
||||
resetPasswordReq.Email = email
|
||||
ok, err := u.service.ResetPassword(&resetPasswordReq)
|
||||
if err != nil {
|
||||
resps.InternalServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
resps.BadRequest(c, "Failed to reset password")
|
||||
return
|
||||
}
|
||||
resps.Ok(c, resps.Success, nil)
|
||||
}
|
||||
|
||||
func (u *UserController) ChangeEmail(ctx context.Context, c *app.RequestContext) {
|
||||
// TODO: 实现修改邮箱功能
|
||||
email := strings.TrimSpace(string(c.GetHeader(constant.HeaderKeyEmail)))
|
||||
if email == "" {
|
||||
resps.BadRequest(c, "Email header is required")
|
||||
return
|
||||
}
|
||||
ok, err := u.service.UpdateEmail(ctx, email)
|
||||
if err != nil {
|
||||
resps.InternalServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
resps.BadRequest(c, "Failed to change email")
|
||||
return
|
||||
}
|
||||
resps.Ok(c, resps.Success, nil)
|
||||
}
|
||||
|
||||
func (u *UserController) GetCaptchaConfig(ctx context.Context, c *app.RequestContext) {
|
||||
|
@ -1,33 +1,32 @@
|
||||
package ctxutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"context"
|
||||
|
||||
"github.com/snowykami/neo-blog/internal/model"
|
||||
"github.com/snowykami/neo-blog/internal/repo"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"github.com/snowykami/neo-blog/internal/model"
|
||||
"github.com/snowykami/neo-blog/internal/repo"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
)
|
||||
|
||||
// GetCurrentUser 从上下文中获取当前用户
|
||||
func GetCurrentUser(ctx context.Context) (*model.User, bool) {
|
||||
val := ctx.Value(constant.ContextKeyUserID)
|
||||
if val == nil {
|
||||
return nil, false
|
||||
}
|
||||
user, err := repo.User.GetUserByID(val.(uint))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
val := ctx.Value(constant.ContextKeyUserID)
|
||||
if val == nil {
|
||||
return nil, false
|
||||
}
|
||||
user, err := repo.User.GetUserByID(val.(uint))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return user, true
|
||||
return user, true
|
||||
}
|
||||
|
||||
// GetCurrentUserID 从上下文中获取当前用户ID
|
||||
func GetCurrentUserID(ctx context.Context) (uint, bool) {
|
||||
user, ok := GetCurrentUser(ctx)
|
||||
if !ok || user == nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return user.ID, true
|
||||
user, ok := GetCurrentUser(ctx)
|
||||
if !ok || user == nil {
|
||||
return 0, false
|
||||
}
|
||||
return user.ID, true
|
||||
}
|
||||
|
@ -1,91 +1,101 @@
|
||||
package dto
|
||||
|
||||
type UserDto struct {
|
||||
ID uint `json:"id"` // 用户ID
|
||||
Username string `json:"username"` // 用户名
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"` // 头像URL
|
||||
Email string `json:"email"` // 邮箱
|
||||
Gender string `json:"gender"`
|
||||
Role string `json:"role"`
|
||||
Language string `json:"language"` // 语言
|
||||
ID uint `json:"id"` // 用户ID
|
||||
Username string `json:"username"` // 用户名
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"` // 头像URL
|
||||
Email string `json:"email"` // 邮箱
|
||||
Gender string `json:"gender"`
|
||||
Role string `json:"role"`
|
||||
Language string `json:"language"` // 语言
|
||||
}
|
||||
|
||||
type UserOidcConfigDto struct {
|
||||
Name string `json:"name"` // OIDC配置名称
|
||||
DisplayName string `json:"display_name"` // OIDC配置显示名称
|
||||
Icon string `json:"icon"` // OIDC配置图标URL
|
||||
LoginUrl string `json:"login_url"` // OIDC登录URL
|
||||
Name string `json:"name"` // OIDC配置名称
|
||||
DisplayName string `json:"display_name"` // OIDC配置显示名称
|
||||
Icon string `json:"icon"` // OIDC配置图标URL
|
||||
LoginUrl string `json:"login_url"` // OIDC登录URL
|
||||
}
|
||||
type UserLoginReq struct {
|
||||
Username string `json:"username"` // username or email
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"` // username or email
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type UserLoginResp struct {
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
User UserDto `json:"user"`
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
User UserDto `json:"user"`
|
||||
}
|
||||
|
||||
type UserRegisterReq struct {
|
||||
Username string `json:"username"` // 用户名
|
||||
Nickname string `json:"nickname"` // 昵称
|
||||
Password string `json:"password"` // 密码
|
||||
Email string `json:"email"` // 邮箱
|
||||
VerificationCode string `json:"verification_code"` // 邮箱验证码
|
||||
Username string `json:"username"` // 用户名
|
||||
Nickname string `json:"nickname"` // 昵称
|
||||
Password string `json:"password"` // 密码
|
||||
Email string `json:"email"` // 邮箱
|
||||
VerificationCode string `json:"verification_code"` // 邮箱验证码
|
||||
}
|
||||
|
||||
type UserRegisterResp struct {
|
||||
Token string `json:"token"` // 访问令牌
|
||||
RefreshToken string `json:"refresh_token"` // 刷新令牌
|
||||
User UserDto `json:"user"` // 用户信息
|
||||
Token string `json:"token"` // 访问令牌
|
||||
RefreshToken string `json:"refresh_token"` // 刷新令牌
|
||||
User UserDto `json:"user"` // 用户信息
|
||||
}
|
||||
|
||||
type VerifyEmailReq struct {
|
||||
Email string `json:"email"` // 邮箱地址
|
||||
Email string `json:"email"` // 邮箱地址
|
||||
}
|
||||
|
||||
type VerifyEmailResp struct {
|
||||
Success bool `json:"success"` // 验证码发送成功与否
|
||||
Success bool `json:"success"` // 验证码发送成功与否
|
||||
}
|
||||
|
||||
type OidcLoginReq struct {
|
||||
Name string `json:"name"` // OIDC配置名称
|
||||
Code string `json:"code"` // OIDC授权码
|
||||
State string `json:"state"`
|
||||
Name string `json:"name"` // OIDC配置名称
|
||||
Code string `json:"code"` // OIDC授权码
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type OidcLoginResp struct {
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
User UserDto `json:"user"`
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
User UserDto `json:"user"`
|
||||
}
|
||||
|
||||
type ListOidcConfigResp struct {
|
||||
OidcConfigs []UserOidcConfigDto `json:"oidc_configs"` // OIDC配置列表
|
||||
OidcConfigs []UserOidcConfigDto `json:"oidc_configs"` // OIDC配置列表
|
||||
}
|
||||
|
||||
type GetUserReq struct {
|
||||
UserID uint `json:"user_id"`
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
|
||||
type GetUserByUsernameReq struct {
|
||||
Username string `json:"username"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type GetUserResp struct {
|
||||
User UserDto `json:"user"` // 用户信息
|
||||
User UserDto `json:"user"` // 用户信息
|
||||
}
|
||||
|
||||
type UpdateUserReq struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
Gender string `json:"gender"`
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
|
||||
type UpdateUserResp struct {
|
||||
User *UserDto `json:"user"` // 更新后的用户信息
|
||||
User *UserDto `json:"user"` // 更新后的用户信息
|
||||
}
|
||||
|
||||
type UpdatePasswordReq struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
type ResetPasswordReq struct {
|
||||
Email string `json:"-" binding:"-"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
@ -1 +1,31 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"github.com/snowykami/neo-blog/pkg/resps"
|
||||
"github.com/snowykami/neo-blog/pkg/utils"
|
||||
)
|
||||
|
||||
// UseEmailVerify 中间件函数,用于邮箱验证,使用前先调用请求发送邮件验证码函数
|
||||
func UseEmailVerify() app.HandlerFunc {
|
||||
return func(ctx context.Context, c *app.RequestContext) {
|
||||
email := string(c.GetHeader(constant.HeaderKeyEmail))
|
||||
verifyCode := string(c.GetHeader(constant.HeaderKeyVerifyCode))
|
||||
if !utils.Env.GetAsBool(constant.EnvKeyEnableEmailVerify, true) {
|
||||
c.Next(ctx)
|
||||
}
|
||||
if email == "" || verifyCode == "" {
|
||||
resps.BadRequest(c, "缺失email和verifyCode")
|
||||
return
|
||||
}
|
||||
ok := utils.VerifyEmailCode(email, verifyCode)
|
||||
if !ok {
|
||||
resps.Unauthorized(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
c.Next(ctx)
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/snowykami/neo-blog/internal/model"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"github.com/snowykami/neo-blog/pkg/utils"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
@ -36,14 +37,14 @@ type DBConfig struct {
|
||||
// loadDBConfig 从配置文件加载数据库配置
|
||||
func loadDBConfig() DBConfig {
|
||||
return DBConfig{
|
||||
Driver: utils.Env.Get("DB_DRIVER", "sqlite"),
|
||||
Path: utils.Env.Get("DB_PATH", "./data/data.db"),
|
||||
Host: utils.Env.Get("DB_HOST", "postgres"),
|
||||
Port: utils.Env.GetAsInt("DB_PORT", 5432),
|
||||
User: utils.Env.Get("DB_USER", "blog"),
|
||||
Password: utils.Env.Get("DB_PASSWORD", "blog"),
|
||||
DBName: utils.Env.Get("DB_NAME", "blog"),
|
||||
SSLMode: utils.Env.Get("DB_SSLMODE", "disable"),
|
||||
Driver: utils.Env.Get(constant.EnvKeyDBDriver, "sqlite"),
|
||||
Path: utils.Env.Get(constant.EnvKeyDBPath, "./data/data.db"),
|
||||
Host: utils.Env.Get(constant.EnvKeyDBHost, "postgres"),
|
||||
Port: utils.Env.GetAsInt(constant.EnvKeyDBPort, 5432),
|
||||
User: utils.Env.Get(constant.EnvKeyDBUser, "blog"),
|
||||
Password: utils.Env.Get(constant.EnvKeyDBPassword, "blog"),
|
||||
DBName: utils.Env.Get(constant.EnvKeyDBName, "blog"),
|
||||
SSLMode: utils.Env.Get(constant.EnvKeyDBSSLMode, "disable"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,10 +7,11 @@ import (
|
||||
)
|
||||
|
||||
func registerUserRoutes(group *route.RouterGroup) {
|
||||
const userRoute = "/user"
|
||||
userController := v1.NewUserController()
|
||||
userGroup := group.Group("/user").Use(middleware.UseAuth(true))
|
||||
userGroupWithoutAuth := group.Group("/user").Use(middleware.UseAuth(false))
|
||||
userGroupWithoutAuthNeedsCaptcha := group.Group("/user").Use(middleware.UseCaptcha())
|
||||
userGroup := group.Group(userRoute).Use(middleware.UseAuth(true))
|
||||
userGroupWithoutAuth := group.Group(userRoute).Use(middleware.UseAuth(false))
|
||||
userGroupWithoutAuthNeedsCaptcha := group.Group(userRoute).Use(middleware.UseCaptcha())
|
||||
{
|
||||
userGroupWithoutAuthNeedsCaptcha.POST("/login", userController.Login)
|
||||
userGroupWithoutAuthNeedsCaptcha.POST("/register", userController.Register)
|
||||
@ -20,10 +21,11 @@ func registerUserRoutes(group *route.RouterGroup) {
|
||||
userGroupWithoutAuth.GET("/oidc/login/:name", userController.OidcLogin)
|
||||
userGroupWithoutAuth.GET("/u/:id", userController.GetUser)
|
||||
userGroupWithoutAuth.GET("/username/:username", userController.GetUserByUsername)
|
||||
userGroup.POST("/logout", userController.Logout)
|
||||
userGroup.GET("/me", userController.GetUser)
|
||||
userGroupWithoutAuth.POST("/logout", userController.Logout)
|
||||
userGroup.PUT("/u/:id", userController.UpdateUser)
|
||||
userGroup.PUT("/password/edit", userController.ChangePassword)
|
||||
userGroup.PUT("/email/edit", userController.ChangeEmail)
|
||||
group.Group(userRoute).Use(middleware.UseEmailVerify()).PUT("/password/reset", userController.ResetPassword) // 不需要登录
|
||||
group.Group(userRoute).Use(middleware.UseAuth(true), middleware.UseEmailVerify()).PUT("/email/edit", userController.ChangeEmail)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@ -8,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/snowykami/neo-blog/internal/ctxutils"
|
||||
"github.com/snowykami/neo-blog/internal/dto"
|
||||
"github.com/snowykami/neo-blog/internal/model"
|
||||
"github.com/snowykami/neo-blog/internal/repo"
|
||||
@ -36,7 +38,7 @@ func (s *UserService) UserLogin(req *dto.UserLoginReq) (*dto.UserLoginResp, erro
|
||||
if user == nil {
|
||||
return nil, errs.ErrNotFound
|
||||
}
|
||||
if utils.Password.VerifyPassword(req.Password, user.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, "default_salt")) {
|
||||
if utils.Password.VerifyPassword(req.Password, user.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, constant.DefaultPasswordSalt)) {
|
||||
token, refreshToken, err := s.generate2Token(user.ID)
|
||||
if err != nil {
|
||||
logrus.Errorln("Failed to generate tokens:", err)
|
||||
@ -55,15 +57,11 @@ func (s *UserService) UserLogin(req *dto.UserLoginReq) (*dto.UserLoginResp, erro
|
||||
|
||||
func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterResp, error) {
|
||||
// 验证邮箱验证码
|
||||
if !utils.Env.GetAsBool("ENABLE_REGISTER", true) {
|
||||
if !utils.Env.GetAsBool(constant.EnvKeyEnableRegister, true) {
|
||||
return nil, errs.ErrForbidden
|
||||
}
|
||||
if utils.Env.GetAsBool("ENABLE_EMAIL_VERIFICATION", true) {
|
||||
ok, err := s.verifyEmail(req.Email, req.VerificationCode)
|
||||
if err != nil {
|
||||
logrus.Errorln("Failed to verify email:", err)
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
if utils.Env.GetAsBool(constant.EnvKeyEnableEmailVerify, true) {
|
||||
ok := utils.VerifyEmailCode(req.Email, req.VerificationCode)
|
||||
if !ok {
|
||||
return nil, errs.New(http.StatusForbidden, "Invalid email verification code", nil)
|
||||
}
|
||||
@ -81,7 +79,7 @@ func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterR
|
||||
return nil, errs.New(http.StatusConflict, "Username or email already exists", nil)
|
||||
}
|
||||
// 创建新用户
|
||||
hashedPassword, err := utils.Password.HashPassword(req.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, "default_salt"))
|
||||
hashedPassword, err := utils.Password.HashPassword(req.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, constant.DefaultPasswordSalt))
|
||||
if err != nil {
|
||||
logrus.Errorln("Failed to hash password:", err)
|
||||
return nil, errs.ErrInternalServer
|
||||
@ -122,19 +120,15 @@ func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterR
|
||||
}
|
||||
|
||||
func (s *UserService) RequestVerifyEmail(req *dto.VerifyEmailReq) (*dto.VerifyEmailResp, error) {
|
||||
generatedVerificationCode := utils.Strings.GenerateRandomStringWithCharset(6, "0123456789abcdef")
|
||||
kv := utils.KV.GetInstance()
|
||||
kv.Set(constant.KVKeyEmailVerificationCode+req.Email, generatedVerificationCode, time.Minute*10)
|
||||
|
||||
verifyCode := utils.RequestEmailVerify(req.Email)
|
||||
template, err := static.RenderTemplate("email/verification-code.tmpl", map[string]interface{}{})
|
||||
if err != nil {
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
if utils.IsDevMode {
|
||||
logrus.Infof("%s's verification code is %s", req.Email, generatedVerificationCode)
|
||||
logrus.Infof("%s's verification code is %s", req.Email, verifyCode)
|
||||
}
|
||||
err = utils.Email.SendEmail(utils.Email.GetEmailConfigFromEnv(), req.Email, "验证你的电子邮件 / Verify your email", template, true)
|
||||
|
||||
if err != nil {
|
||||
return nil, errs.ErrInternalServer
|
||||
}
|
||||
@ -373,6 +367,56 @@ func (s *UserService) UpdateUser(req *dto.UpdateUserReq) (*dto.UpdateUserResp, e
|
||||
return &dto.UpdateUserResp{}, nil
|
||||
}
|
||||
|
||||
func (s *UserService) UpdatePassword(ctx context.Context, req *dto.UpdatePasswordReq) (bool, error) {
|
||||
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
||||
if !ok || currentUser == nil {
|
||||
return false, errs.ErrUnauthorized
|
||||
}
|
||||
if !utils.Password.VerifyPassword(req.OldPassword, currentUser.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, constant.DefaultPasswordSalt)) {
|
||||
return false, errs.New(http.StatusForbidden, "Old password is incorrect", nil)
|
||||
}
|
||||
hashedPassword, err := utils.Password.HashPassword(req.NewPassword, utils.Env.Get(constant.EnvKeyPasswordSalt, constant.DefaultPasswordSalt))
|
||||
if err != nil {
|
||||
logrus.Errorln("Failed to update password:", err)
|
||||
}
|
||||
currentUser.Password = hashedPassword
|
||||
err = repo.GetDB().Save(currentUser).Error
|
||||
if err != nil {
|
||||
return false, errs.ErrInternalServer
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *UserService) ResetPassword(req *dto.ResetPasswordReq) (bool, error) {
|
||||
user, err := repo.User.GetUserByEmail(req.Email)
|
||||
if err != nil {
|
||||
return false, errs.ErrInternalServer
|
||||
}
|
||||
hashedPassword, err := utils.Password.HashPassword(req.NewPassword, utils.Env.Get(constant.EnvKeyPasswordSalt, constant.DefaultPasswordSalt))
|
||||
if err != nil {
|
||||
return false, errs.ErrInternalServer
|
||||
}
|
||||
user.Password = hashedPassword
|
||||
err = repo.User.UpdateUser(user)
|
||||
if err != nil {
|
||||
return false, errs.ErrInternalServer
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateEmail(ctx context.Context, email string) (bool, error) {
|
||||
currentUser, ok := ctxutils.GetCurrentUser(ctx)
|
||||
if !ok || currentUser == nil {
|
||||
return false, errs.ErrUnauthorized
|
||||
}
|
||||
currentUser.Email = email
|
||||
err := repo.GetDB().Save(currentUser).Error
|
||||
if err != nil {
|
||||
return false, errs.ErrInternalServer
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *UserService) generate2Token(userID uint) (string, string, error) {
|
||||
token := utils.Jwt.NewClaims(userID, "", false, time.Duration(utils.Env.GetAsInt(constant.EnvKeyTokenDuration, constant.EnvKeyTokenDurationDefault))*time.Second)
|
||||
tokenString, err := token.ToString()
|
||||
@ -390,12 +434,3 @@ func (s *UserService) generate2Token(userID uint) (string, string, error) {
|
||||
}
|
||||
return tokenString, refreshTokenString, nil
|
||||
}
|
||||
|
||||
func (s *UserService) verifyEmail(email, code string) (bool, error) {
|
||||
kv := utils.KV.GetInstance()
|
||||
verificationCode, ok := kv.Get(constant.KVKeyEmailVerificationCode + email)
|
||||
if !ok || verificationCode != code {
|
||||
return false, errs.New(http.StatusForbidden, "Invalid email verification code", nil)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user